Skip to content

Commit

Permalink
Classify the langword attribute value in DocComments (#76678)
Browse files Browse the repository at this point in the history
Fixes #63885
  • Loading branch information
JoeRobich authored Jan 9, 2025
2 parents 0da1e63 + e359e4a commit cb362e9
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,89 @@ await TestAsync(code,
Punctuation.CloseCurly);
}

[Theory]
[InlineData(TestHost.InProcess, "true", false)]
[InlineData(TestHost.OutOfProcess, "true", false)]
[InlineData(TestHost.InProcess, "return", true)]
[InlineData(TestHost.OutOfProcess, "return", true)]
[InlineData(TestHost.InProcess, "with", false)]
[InlineData(TestHost.OutOfProcess, "with", false)]
public async Task XmlDocComment_LangWordAttribute_Keywords(TestHost testHost, string langword, bool isControlKeyword)
{
await TestAsync(
$$"""
/// <summary>
/// <see langword="{{langword}}"/>
/// </summary>
class MyClass
{
}
""",
testHost,
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("see"),
XmlDoc.AttributeName("langword"),
XmlDoc.Delimiter("="),
XmlDoc.AttributeQuotes("\""),
isControlKeyword ? ControlKeyword(langword) : Keyword(langword),
XmlDoc.AttributeQuotes("\""),
XmlDoc.Delimiter("/>"),
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("</"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
Keyword("class"),
Class("MyClass"),
Punctuation.OpenCurly,
Punctuation.CloseCurly);
}

[Theory, CombinatorialData]
public async Task XmlDocComment_LangWordAttribute_NonKeyword(TestHost testHost)
{
await TestAsync(
"""
/// <summary>
/// <see langword="MyWord"/>
/// </summary>
class MyClass
{
}
""", testHost,
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("see"),
XmlDoc.AttributeName("langword"),
XmlDoc.Delimiter("="),
XmlDoc.AttributeQuotes("\""),
XmlDoc.AttributeValue("MyWord"),
XmlDoc.AttributeQuotes("\""),
XmlDoc.Delimiter("/>"),
XmlDoc.Delimiter("///"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("</"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
Keyword("class"),
Class("MyClass"),
Punctuation.OpenCurly,
Punctuation.CloseCurly);
}

[Theory, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/531155")]
[CombinatorialData]
public async Task XmlDocComment_ExteriorTriviaInsideCRef(TestHost testHost)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2670,6 +2670,90 @@ End Class"
Keyword("Class"))
End Function

<Theory>
<InlineData(TestHost.InProcess, "True", False)>
<InlineData(TestHost.OutOfProcess, "True", False)>
<InlineData(TestHost.InProcess, "Return", True)>
<InlineData(TestHost.OutOfProcess, "Return", True)>
<InlineData(TestHost.InProcess, "All", False)>
<InlineData(TestHost.OutOfProcess, "All", False)>
Public Async Function TestXmlDocComment_LangWordAttribute_Keywords(testHost As TestHost, langword As String, isControlKeyword As Boolean) As Task
Dim code =
$"''' <summary>
''' <see langword=""{langword}"" />
''' </summary>
Class MyClass
End Class"

Await TestAsync(code,
testHost,
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("see"),
XmlDoc.Name(" "),
XmlDoc.AttributeName("langword"),
XmlDoc.Delimiter("="),
XmlDoc.AttributeQuotes(""""),
If(isControlKeyword, ControlKeyword(langword), Keyword(langword)),
XmlDoc.AttributeQuotes(""""),
XmlDoc.AttributeQuotes(" "),
XmlDoc.Delimiter("/>"),
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("</"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
Keyword("Class"),
[Class]("MyClass"),
Keyword("End"),
Keyword("Class"))
End Function

<Theory, CombinatorialData>
Public Async Function TestXmlDocComment_LangWordAttribute_NonKeyword(testHost As TestHost) As Task
Dim code =
"''' <summary>
''' <see langword=""MyWord"" />
''' </summary>
Class MyClass
End Class"

Await TestAsync(code,
testHost,
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("<"),
XmlDoc.Name("see"),
XmlDoc.Name(" "),
XmlDoc.AttributeName("langword"),
XmlDoc.Delimiter("="),
XmlDoc.AttributeQuotes(""""),
XmlDoc.AttributeValue("MyWord"),
XmlDoc.AttributeQuotes(""""),
XmlDoc.AttributeQuotes(" "),
XmlDoc.Delimiter("/>"),
XmlDoc.Delimiter("'''"),
XmlDoc.Text(" "),
XmlDoc.Delimiter("</"),
XmlDoc.Name("summary"),
XmlDoc.Delimiter(">"),
Keyword("Class"),
[Class]("MyClass"),
Keyword("End"),
Keyword("Class"))
End Function

<Theory, CombinatorialData>
Public Async Function TestXmlDocComment_EmptyElementAttributesWithExteriorTrivia(testHost As TestHost) As Task
Dim code =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ private static bool IsControlKeyword(SyntaxToken token)
IsControlKeywordKind(token.Kind()) &&
IsControlStatementKind(token.Parent.Kind());

private static bool IsControlKeywordKind(SyntaxKind kind)
public static bool IsControlKeywordKind(SyntaxKind kind)
{
switch (kind)
{
Expand Down Expand Up @@ -94,7 +94,7 @@ private static bool IsControlKeywordKind(SyntaxKind kind)
}
}

private static bool IsControlStatementKind(SyntaxKind kind)
public static bool IsControlStatementKind(SyntaxKind kind)
{
switch (kind)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,13 @@ private void ClassifyXmlAttribute(XmlAttributeSyntax attribute)
switch (attribute.Kind())
{
case SyntaxKind.XmlTextAttribute:
ClassifyXmlTextTokens(((XmlTextAttributeSyntax)attribute).TextTokens);
// Since the langword attribute in `<see langword="..." />` is not parsed into its own
// SyntaxNode as cref is, we need to handle it specially.
if (IsLangWordAttribute(attribute))
ClassifyLangWordTextTokenList(((XmlTextAttributeSyntax)attribute).TextTokens);
else
ClassifyXmlTextTokens(((XmlTextAttributeSyntax)attribute).TextTokens);

break;
case SyntaxKind.XmlCrefAttribute:
ClassifyNode(((XmlCrefAttributeSyntax)attribute).Cref);
Expand All @@ -264,6 +270,47 @@ private void ClassifyXmlAttribute(XmlAttributeSyntax attribute)
}

AddXmlClassification(attribute.EndQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes);

static bool IsLangWordAttribute(XmlAttributeSyntax attribute)
{
return attribute.Name.LocalName.Text == DocumentationCommentXmlNames.LangwordAttributeName && IsSeeElement(attribute.Parent);
}

static bool IsSeeElement(SyntaxNode? node)
{
return node is XmlElementStartTagSyntax { Name: XmlNameSyntax { Prefix: null, LocalName: SyntaxToken { Text: DocumentationCommentXmlNames.SeeElementName } } }
|| node is XmlEmptyElementSyntax { Name: XmlNameSyntax { Prefix: null, LocalName: SyntaxToken { Text: DocumentationCommentXmlNames.SeeElementName } } };
}
}

private void ClassifyLangWordTextTokenList(SyntaxTokenList list)
{
foreach (var token in list)
{
if (token.HasLeadingTrivia)
ClassifyXmlTrivia(token.LeadingTrivia);

ClassifyLangWordTextToken(token);

if (token.HasTrailingTrivia)
ClassifyXmlTrivia(token.TrailingTrivia);
}
}

private void ClassifyLangWordTextToken(SyntaxToken token)
{
var kind = SyntaxFacts.GetKeywordKind(token.Text);
if (kind is SyntaxKind.None)
kind = SyntaxFacts.GetContextualKeywordKind(token.Text);

if (kind is SyntaxKind.None)
{
ClassifyXmlTextToken(token);
return;
}

var isControlKeyword = ClassificationHelpers.IsControlKeywordKind(kind) || ClassificationHelpers.IsControlStatementKind(kind);
AddClassification(token, isControlKeyword ? ClassificationTypeNames.ControlKeyword : ClassificationTypeNames.Keyword);
}

private void ClassifyXmlText(XmlTextSyntax node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
''' <summary>
''' Determine if the kind represents a control keyword
''' </summary>
Private Function IsControlKeywordKind(kind As SyntaxKind) As Boolean
Public Function IsControlKeywordKind(kind As SyntaxKind) As Boolean
Select Case kind
Case _
SyntaxKind.CaseKeyword,
Expand Down Expand Up @@ -119,7 +119,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
''' <summary>
''' Determine if the kind represents a control statement
''' </summary>
Private Function IsControlStatementKind(kind As SyntaxKind) As Boolean
Public Function IsControlStatementKind(kind As SyntaxKind) As Boolean
Select Case kind
Case _
SyntaxKind.CallStatement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
If attribute IsNot Nothing Then
Select Case attribute.Kind
Case SyntaxKind.XmlAttribute
ClassifyAttribute(DirectCast(attribute, XmlAttributeSyntax))
Dim xmlAttribute = DirectCast(attribute, XmlAttributeSyntax)
If IsLangWordAttribute(xmlAttribute) Then
ClassifyLangWordAttribute(xmlAttribute)

Else
ClassifyAttribute(xmlAttribute)
End If

Case SyntaxKind.XmlCrefAttribute
ClassifyCrefAttribute(DirectCast(attribute, XmlCrefAttributeSyntax))
Expand All @@ -227,6 +233,71 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
ClassifyXmlNode(attribute.Value)
End Sub

Private Shared Function IsLangWordAttribute(attribute As XmlAttributeSyntax) As Boolean
Dim nameNode = DirectCast(attribute.Name, XmlNameSyntax)
If nameNode.LocalName.Text <> DocumentationCommentXmlNames.LangwordAttributeName Then
Return False
End If

Dim startTag = TryCast(attribute.Parent, XmlElementStartTagSyntax)
If (startTag IsNot Nothing) Then
Dim startTagName = TryCast(startTag.Name, XmlNameSyntax)
Return startTagName IsNot Nothing AndAlso
startTagName.Prefix Is Nothing AndAlso
startTagName.LocalName.Text = DocumentationCommentXmlNames.SeeElementName
End If

Dim emptyElement = TryCast(attribute.Parent, XmlEmptyElementSyntax)
If (emptyElement IsNot Nothing) Then
Dim emptyElementName = TryCast(emptyElement.Name, XmlNameSyntax)
Return emptyElementName IsNot Nothing AndAlso
emptyElementName.Prefix Is Nothing AndAlso
emptyElementName.LocalName.Text = DocumentationCommentXmlNames.SeeElementName
End If

Return False
End Function

Private Sub ClassifyLangWordAttribute(attribute As XmlAttributeSyntax)
ClassifyXmlNode(attribute.Name)
AddXmlClassification(attribute.EqualsToken, ClassificationTypeNames.XmlDocCommentDelimiter)

Dim node = (DirectCast(attribute.Value, XmlStringSyntax))

AddXmlClassification(node.StartQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes)
ClassifyLangWordTextTokenList(node.TextTokens)
AddXmlClassification(node.EndQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes)
End Sub

Private Sub ClassifyLangWordTextTokenList(list As SyntaxTokenList)
For Each token In list
If (token.HasLeadingTrivia) Then
ClassifyXmlTrivia(token.LeadingTrivia, ClassificationTypeNames.XmlDocCommentText)
End If

ClassifyLangWordTextToken(token)

If (token.HasTrailingTrivia) Then
ClassifyXmlTrivia(token.TrailingTrivia, ClassificationTypeNames.XmlDocCommentText)
End If
Next
End Sub

Private Sub ClassifyLangWordTextToken(token As SyntaxToken)
Dim kind = SyntaxFacts.GetKeywordKind(token.Text)
If kind = SyntaxKind.None Then
kind = SyntaxFacts.GetContextualKeywordKind(token.Text)
End If

If kind = SyntaxKind.None Then
AddXmlClassification(token, ClassificationTypeNames.XmlDocCommentAttributeValue)
Return
End If

Dim isControlKeyword = IsControlKeywordKind(kind) Or IsControlStatementKind(kind)
AddXmlClassification(token, If(isControlKeyword, ClassificationTypeNames.ControlKeyword, ClassificationTypeNames.Keyword))
End Sub

Private Sub ClassifyCrefAttribute(attribute As XmlCrefAttributeSyntax)
ClassifyXmlNode(attribute.Name)
AddXmlClassification(attribute.EqualsToken, ClassificationTypeNames.XmlDocCommentDelimiter)
Expand Down

0 comments on commit cb362e9

Please sign in to comment.