Skip to content

Commit cb362e9

Browse files
authored
Classify the langword attribute value in DocComments (#76678)
Fixes #63885
2 parents 0da1e63 + e359e4a commit cb362e9

File tree

6 files changed

+291
-6
lines changed

6 files changed

+291
-6
lines changed

src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,89 @@ await TestAsync(code,
17121712
Punctuation.CloseCurly);
17131713
}
17141714

1715+
[Theory]
1716+
[InlineData(TestHost.InProcess, "true", false)]
1717+
[InlineData(TestHost.OutOfProcess, "true", false)]
1718+
[InlineData(TestHost.InProcess, "return", true)]
1719+
[InlineData(TestHost.OutOfProcess, "return", true)]
1720+
[InlineData(TestHost.InProcess, "with", false)]
1721+
[InlineData(TestHost.OutOfProcess, "with", false)]
1722+
public async Task XmlDocComment_LangWordAttribute_Keywords(TestHost testHost, string langword, bool isControlKeyword)
1723+
{
1724+
await TestAsync(
1725+
$$"""
1726+
/// <summary>
1727+
/// <see langword="{{langword}}"/>
1728+
/// </summary>
1729+
class MyClass
1730+
{
1731+
}
1732+
""",
1733+
testHost,
1734+
XmlDoc.Delimiter("///"),
1735+
XmlDoc.Text(" "),
1736+
XmlDoc.Delimiter("<"),
1737+
XmlDoc.Name("summary"),
1738+
XmlDoc.Delimiter(">"),
1739+
XmlDoc.Delimiter("///"),
1740+
XmlDoc.Text(" "),
1741+
XmlDoc.Delimiter("<"),
1742+
XmlDoc.Name("see"),
1743+
XmlDoc.AttributeName("langword"),
1744+
XmlDoc.Delimiter("="),
1745+
XmlDoc.AttributeQuotes("\""),
1746+
isControlKeyword ? ControlKeyword(langword) : Keyword(langword),
1747+
XmlDoc.AttributeQuotes("\""),
1748+
XmlDoc.Delimiter("/>"),
1749+
XmlDoc.Delimiter("///"),
1750+
XmlDoc.Text(" "),
1751+
XmlDoc.Delimiter("</"),
1752+
XmlDoc.Name("summary"),
1753+
XmlDoc.Delimiter(">"),
1754+
Keyword("class"),
1755+
Class("MyClass"),
1756+
Punctuation.OpenCurly,
1757+
Punctuation.CloseCurly);
1758+
}
1759+
1760+
[Theory, CombinatorialData]
1761+
public async Task XmlDocComment_LangWordAttribute_NonKeyword(TestHost testHost)
1762+
{
1763+
await TestAsync(
1764+
"""
1765+
/// <summary>
1766+
/// <see langword="MyWord"/>
1767+
/// </summary>
1768+
class MyClass
1769+
{
1770+
}
1771+
""", testHost,
1772+
XmlDoc.Delimiter("///"),
1773+
XmlDoc.Text(" "),
1774+
XmlDoc.Delimiter("<"),
1775+
XmlDoc.Name("summary"),
1776+
XmlDoc.Delimiter(">"),
1777+
XmlDoc.Delimiter("///"),
1778+
XmlDoc.Text(" "),
1779+
XmlDoc.Delimiter("<"),
1780+
XmlDoc.Name("see"),
1781+
XmlDoc.AttributeName("langword"),
1782+
XmlDoc.Delimiter("="),
1783+
XmlDoc.AttributeQuotes("\""),
1784+
XmlDoc.AttributeValue("MyWord"),
1785+
XmlDoc.AttributeQuotes("\""),
1786+
XmlDoc.Delimiter("/>"),
1787+
XmlDoc.Delimiter("///"),
1788+
XmlDoc.Text(" "),
1789+
XmlDoc.Delimiter("</"),
1790+
XmlDoc.Name("summary"),
1791+
XmlDoc.Delimiter(">"),
1792+
Keyword("class"),
1793+
Class("MyClass"),
1794+
Punctuation.OpenCurly,
1795+
Punctuation.CloseCurly);
1796+
}
1797+
17151798
[Theory, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/531155")]
17161799
[CombinatorialData]
17171800
public async Task XmlDocComment_ExteriorTriviaInsideCRef(TestHost testHost)

src/EditorFeatures/VisualBasicTest/Classification/SyntacticClassifierTests.vb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,6 +2670,90 @@ End Class"
26702670
Keyword("Class"))
26712671
End Function
26722672

2673+
<Theory>
2674+
<InlineData(TestHost.InProcess, "True", False)>
2675+
<InlineData(TestHost.OutOfProcess, "True", False)>
2676+
<InlineData(TestHost.InProcess, "Return", True)>
2677+
<InlineData(TestHost.OutOfProcess, "Return", True)>
2678+
<InlineData(TestHost.InProcess, "All", False)>
2679+
<InlineData(TestHost.OutOfProcess, "All", False)>
2680+
Public Async Function TestXmlDocComment_LangWordAttribute_Keywords(testHost As TestHost, langword As String, isControlKeyword As Boolean) As Task
2681+
Dim code =
2682+
$"''' <summary>
2683+
''' <see langword=""{langword}"" />
2684+
''' </summary>
2685+
Class MyClass
2686+
End Class"
2687+
2688+
Await TestAsync(code,
2689+
testHost,
2690+
XmlDoc.Delimiter("'''"),
2691+
XmlDoc.Text(" "),
2692+
XmlDoc.Delimiter("<"),
2693+
XmlDoc.Name("summary"),
2694+
XmlDoc.Delimiter(">"),
2695+
XmlDoc.Delimiter("'''"),
2696+
XmlDoc.Text(" "),
2697+
XmlDoc.Delimiter("<"),
2698+
XmlDoc.Name("see"),
2699+
XmlDoc.Name(" "),
2700+
XmlDoc.AttributeName("langword"),
2701+
XmlDoc.Delimiter("="),
2702+
XmlDoc.AttributeQuotes(""""),
2703+
If(isControlKeyword, ControlKeyword(langword), Keyword(langword)),
2704+
XmlDoc.AttributeQuotes(""""),
2705+
XmlDoc.AttributeQuotes(" "),
2706+
XmlDoc.Delimiter("/>"),
2707+
XmlDoc.Delimiter("'''"),
2708+
XmlDoc.Text(" "),
2709+
XmlDoc.Delimiter("</"),
2710+
XmlDoc.Name("summary"),
2711+
XmlDoc.Delimiter(">"),
2712+
Keyword("Class"),
2713+
[Class]("MyClass"),
2714+
Keyword("End"),
2715+
Keyword("Class"))
2716+
End Function
2717+
2718+
<Theory, CombinatorialData>
2719+
Public Async Function TestXmlDocComment_LangWordAttribute_NonKeyword(testHost As TestHost) As Task
2720+
Dim code =
2721+
"''' <summary>
2722+
''' <see langword=""MyWord"" />
2723+
''' </summary>
2724+
Class MyClass
2725+
End Class"
2726+
2727+
Await TestAsync(code,
2728+
testHost,
2729+
XmlDoc.Delimiter("'''"),
2730+
XmlDoc.Text(" "),
2731+
XmlDoc.Delimiter("<"),
2732+
XmlDoc.Name("summary"),
2733+
XmlDoc.Delimiter(">"),
2734+
XmlDoc.Delimiter("'''"),
2735+
XmlDoc.Text(" "),
2736+
XmlDoc.Delimiter("<"),
2737+
XmlDoc.Name("see"),
2738+
XmlDoc.Name(" "),
2739+
XmlDoc.AttributeName("langword"),
2740+
XmlDoc.Delimiter("="),
2741+
XmlDoc.AttributeQuotes(""""),
2742+
XmlDoc.AttributeValue("MyWord"),
2743+
XmlDoc.AttributeQuotes(""""),
2744+
XmlDoc.AttributeQuotes(" "),
2745+
XmlDoc.Delimiter("/>"),
2746+
XmlDoc.Delimiter("'''"),
2747+
XmlDoc.Text(" "),
2748+
XmlDoc.Delimiter("</"),
2749+
XmlDoc.Name("summary"),
2750+
XmlDoc.Delimiter(">"),
2751+
Keyword("Class"),
2752+
[Class]("MyClass"),
2753+
Keyword("End"),
2754+
Keyword("Class"))
2755+
End Function
2756+
26732757
<Theory, CombinatorialData>
26742758
Public Async Function TestXmlDocComment_EmptyElementAttributesWithExteriorTrivia(testHost As TestHost) As Task
26752759
Dim code =

src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private static bool IsControlKeyword(SyntaxToken token)
6363
IsControlKeywordKind(token.Kind()) &&
6464
IsControlStatementKind(token.Parent.Kind());
6565

66-
private static bool IsControlKeywordKind(SyntaxKind kind)
66+
public static bool IsControlKeywordKind(SyntaxKind kind)
6767
{
6868
switch (kind)
6969
{
@@ -94,7 +94,7 @@ private static bool IsControlKeywordKind(SyntaxKind kind)
9494
}
9595
}
9696

97-
private static bool IsControlStatementKind(SyntaxKind kind)
97+
public static bool IsControlStatementKind(SyntaxKind kind)
9898
{
9999
switch (kind)
100100
{

src/Workspaces/CSharp/Portable/Classification/Worker_DocumentationComments.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,13 @@ private void ClassifyXmlAttribute(XmlAttributeSyntax attribute)
253253
switch (attribute.Kind())
254254
{
255255
case SyntaxKind.XmlTextAttribute:
256-
ClassifyXmlTextTokens(((XmlTextAttributeSyntax)attribute).TextTokens);
256+
// Since the langword attribute in `<see langword="..." />` is not parsed into its own
257+
// SyntaxNode as cref is, we need to handle it specially.
258+
if (IsLangWordAttribute(attribute))
259+
ClassifyLangWordTextTokenList(((XmlTextAttributeSyntax)attribute).TextTokens);
260+
else
261+
ClassifyXmlTextTokens(((XmlTextAttributeSyntax)attribute).TextTokens);
262+
257263
break;
258264
case SyntaxKind.XmlCrefAttribute:
259265
ClassifyNode(((XmlCrefAttributeSyntax)attribute).Cref);
@@ -264,6 +270,47 @@ private void ClassifyXmlAttribute(XmlAttributeSyntax attribute)
264270
}
265271

266272
AddXmlClassification(attribute.EndQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes);
273+
274+
static bool IsLangWordAttribute(XmlAttributeSyntax attribute)
275+
{
276+
return attribute.Name.LocalName.Text == DocumentationCommentXmlNames.LangwordAttributeName && IsSeeElement(attribute.Parent);
277+
}
278+
279+
static bool IsSeeElement(SyntaxNode? node)
280+
{
281+
return node is XmlElementStartTagSyntax { Name: XmlNameSyntax { Prefix: null, LocalName: SyntaxToken { Text: DocumentationCommentXmlNames.SeeElementName } } }
282+
|| node is XmlEmptyElementSyntax { Name: XmlNameSyntax { Prefix: null, LocalName: SyntaxToken { Text: DocumentationCommentXmlNames.SeeElementName } } };
283+
}
284+
}
285+
286+
private void ClassifyLangWordTextTokenList(SyntaxTokenList list)
287+
{
288+
foreach (var token in list)
289+
{
290+
if (token.HasLeadingTrivia)
291+
ClassifyXmlTrivia(token.LeadingTrivia);
292+
293+
ClassifyLangWordTextToken(token);
294+
295+
if (token.HasTrailingTrivia)
296+
ClassifyXmlTrivia(token.TrailingTrivia);
297+
}
298+
}
299+
300+
private void ClassifyLangWordTextToken(SyntaxToken token)
301+
{
302+
var kind = SyntaxFacts.GetKeywordKind(token.Text);
303+
if (kind is SyntaxKind.None)
304+
kind = SyntaxFacts.GetContextualKeywordKind(token.Text);
305+
306+
if (kind is SyntaxKind.None)
307+
{
308+
ClassifyXmlTextToken(token);
309+
return;
310+
}
311+
312+
var isControlKeyword = ClassificationHelpers.IsControlKeywordKind(kind) || ClassificationHelpers.IsControlStatementKind(kind);
313+
AddClassification(token, isControlKeyword ? ClassificationTypeNames.ControlKeyword : ClassificationTypeNames.Keyword);
267314
}
268315

269316
private void ClassifyXmlText(XmlTextSyntax node)

src/Workspaces/VisualBasic/Portable/Classification/ClassificationHelpers.vb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
7878
''' <summary>
7979
''' Determine if the kind represents a control keyword
8080
''' </summary>
81-
Private Function IsControlKeywordKind(kind As SyntaxKind) As Boolean
81+
Public Function IsControlKeywordKind(kind As SyntaxKind) As Boolean
8282
Select Case kind
8383
Case _
8484
SyntaxKind.CaseKeyword,
@@ -119,7 +119,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
119119
''' <summary>
120120
''' Determine if the kind represents a control statement
121121
''' </summary>
122-
Private Function IsControlStatementKind(kind As SyntaxKind) As Boolean
122+
Public Function IsControlStatementKind(kind As SyntaxKind) As Boolean
123123
Select Case kind
124124
Case _
125125
SyntaxKind.CallStatement,

src/Workspaces/VisualBasic/Portable/Classification/Worker.DocumentationCommentClassifier.vb

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
210210
If attribute IsNot Nothing Then
211211
Select Case attribute.Kind
212212
Case SyntaxKind.XmlAttribute
213-
ClassifyAttribute(DirectCast(attribute, XmlAttributeSyntax))
213+
Dim xmlAttribute = DirectCast(attribute, XmlAttributeSyntax)
214+
If IsLangWordAttribute(xmlAttribute) Then
215+
ClassifyLangWordAttribute(xmlAttribute)
216+
217+
Else
218+
ClassifyAttribute(xmlAttribute)
219+
End If
214220

215221
Case SyntaxKind.XmlCrefAttribute
216222
ClassifyCrefAttribute(DirectCast(attribute, XmlCrefAttributeSyntax))
@@ -227,6 +233,71 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Classification
227233
ClassifyXmlNode(attribute.Value)
228234
End Sub
229235

236+
Private Shared Function IsLangWordAttribute(attribute As XmlAttributeSyntax) As Boolean
237+
Dim nameNode = DirectCast(attribute.Name, XmlNameSyntax)
238+
If nameNode.LocalName.Text <> DocumentationCommentXmlNames.LangwordAttributeName Then
239+
Return False
240+
End If
241+
242+
Dim startTag = TryCast(attribute.Parent, XmlElementStartTagSyntax)
243+
If (startTag IsNot Nothing) Then
244+
Dim startTagName = TryCast(startTag.Name, XmlNameSyntax)
245+
Return startTagName IsNot Nothing AndAlso
246+
startTagName.Prefix Is Nothing AndAlso
247+
startTagName.LocalName.Text = DocumentationCommentXmlNames.SeeElementName
248+
End If
249+
250+
Dim emptyElement = TryCast(attribute.Parent, XmlEmptyElementSyntax)
251+
If (emptyElement IsNot Nothing) Then
252+
Dim emptyElementName = TryCast(emptyElement.Name, XmlNameSyntax)
253+
Return emptyElementName IsNot Nothing AndAlso
254+
emptyElementName.Prefix Is Nothing AndAlso
255+
emptyElementName.LocalName.Text = DocumentationCommentXmlNames.SeeElementName
256+
End If
257+
258+
Return False
259+
End Function
260+
261+
Private Sub ClassifyLangWordAttribute(attribute As XmlAttributeSyntax)
262+
ClassifyXmlNode(attribute.Name)
263+
AddXmlClassification(attribute.EqualsToken, ClassificationTypeNames.XmlDocCommentDelimiter)
264+
265+
Dim node = (DirectCast(attribute.Value, XmlStringSyntax))
266+
267+
AddXmlClassification(node.StartQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes)
268+
ClassifyLangWordTextTokenList(node.TextTokens)
269+
AddXmlClassification(node.EndQuoteToken, ClassificationTypeNames.XmlDocCommentAttributeQuotes)
270+
End Sub
271+
272+
Private Sub ClassifyLangWordTextTokenList(list As SyntaxTokenList)
273+
For Each token In list
274+
If (token.HasLeadingTrivia) Then
275+
ClassifyXmlTrivia(token.LeadingTrivia, ClassificationTypeNames.XmlDocCommentText)
276+
End If
277+
278+
ClassifyLangWordTextToken(token)
279+
280+
If (token.HasTrailingTrivia) Then
281+
ClassifyXmlTrivia(token.TrailingTrivia, ClassificationTypeNames.XmlDocCommentText)
282+
End If
283+
Next
284+
End Sub
285+
286+
Private Sub ClassifyLangWordTextToken(token As SyntaxToken)
287+
Dim kind = SyntaxFacts.GetKeywordKind(token.Text)
288+
If kind = SyntaxKind.None Then
289+
kind = SyntaxFacts.GetContextualKeywordKind(token.Text)
290+
End If
291+
292+
If kind = SyntaxKind.None Then
293+
AddXmlClassification(token, ClassificationTypeNames.XmlDocCommentAttributeValue)
294+
Return
295+
End If
296+
297+
Dim isControlKeyword = IsControlKeywordKind(kind) Or IsControlStatementKind(kind)
298+
AddXmlClassification(token, If(isControlKeyword, ClassificationTypeNames.ControlKeyword, ClassificationTypeNames.Keyword))
299+
End Sub
300+
230301
Private Sub ClassifyCrefAttribute(attribute As XmlCrefAttributeSyntax)
231302
ClassifyXmlNode(attribute.Name)
232303
AddXmlClassification(attribute.EqualsToken, ClassificationTypeNames.XmlDocCommentDelimiter)

0 commit comments

Comments
 (0)