Skip to content

Commit 58d2cf5

Browse files
authored
Merge pull request #32 from sharwell/use-xml-syntax
Implement DOC200 (Use XML Documentation Syntax)
2 parents 51cd8fa + 2d43bcd commit 58d2cf5

11 files changed

Lines changed: 695 additions & 0 deletions

File tree

DOCUMENTATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ DocumentationAnalyzers provides warnings that indicate documentation rule violat
66

77
Rules related to the style of documentation comments.
88

9+
**[Portability Rules (DOC200-)](docs/PortabilityRules.md)**
10+
11+
Rules related to the portability of documentation comments.
12+
913
**[Refactorings (DOC900-)](docs/Refactorings.md)**
1014

1115
Additional refactorings provided when the analyzer is installed.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.PortabilityRules
5+
{
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using DocumentationAnalyzers.Helpers;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.CSharp;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
17+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC200CodeFixProvider))]
18+
[Shared]
19+
internal class DOC200CodeFixProvider : CodeFixProvider
20+
{
21+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
22+
= ImmutableArray.Create(DOC200UseXmlDocumentationSyntax.DiagnosticId);
23+
24+
public override FixAllProvider GetFixAllProvider()
25+
=> CustomFixAllProviders.BatchFixer;
26+
27+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
28+
{
29+
foreach (var diagnostic in context.Diagnostics)
30+
{
31+
if (!FixableDiagnosticIds.Contains(diagnostic.Id))
32+
{
33+
continue;
34+
}
35+
36+
context.RegisterCodeFix(
37+
CodeAction.Create(
38+
PortabilityResources.DOC200CodeFix,
39+
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
40+
nameof(DOC200CodeFixProvider)),
41+
diagnostic);
42+
}
43+
44+
return SpecializedTasks.CompletedTask;
45+
}
46+
47+
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
48+
{
49+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
50+
SyntaxToken token = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true);
51+
52+
var xmlElement = token.Parent.FirstAncestorOrSelf<XmlElementSyntax>();
53+
var oldStartToken = xmlElement.StartTag.Name.LocalName;
54+
55+
string newIdentifier;
56+
switch (oldStartToken.ValueText)
57+
{
58+
case "p":
59+
newIdentifier = XmlCommentHelper.ParaXmlTag;
60+
break;
61+
62+
case "tt":
63+
newIdentifier = XmlCommentHelper.CXmlTag;
64+
break;
65+
66+
case "pre":
67+
newIdentifier = XmlCommentHelper.CodeXmlTag;
68+
break;
69+
70+
case "ul":
71+
case "ol":
72+
newIdentifier = XmlCommentHelper.ListXmlTag;
73+
break;
74+
75+
default:
76+
// Not handled
77+
return document;
78+
}
79+
80+
var newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, newIdentifier, oldStartToken.TrailingTrivia);
81+
var newXmlElement = xmlElement.ReplaceToken(oldStartToken, newStartToken);
82+
83+
var oldEndToken = newXmlElement.EndTag.Name.LocalName;
84+
var newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, newIdentifier, oldEndToken.TrailingTrivia);
85+
newXmlElement = newXmlElement.ReplaceToken(oldEndToken, newEndToken);
86+
87+
if (newIdentifier == XmlCommentHelper.ListXmlTag)
88+
{
89+
// Add an attribute for the list kind
90+
string listType = oldStartToken.ValueText == "ol" ? "number" : "bullet";
91+
newXmlElement = newXmlElement.WithStartTag(newXmlElement.StartTag.AddAttributes(XmlSyntaxFactory.TextAttribute(XmlCommentHelper.TypeAttributeName, listType)));
92+
93+
// Replace each <li>...</li> element with <item><description>...</description></item>
94+
for (int i = 0; i < newXmlElement.Content.Count; i++)
95+
{
96+
if (newXmlElement.Content[i] is XmlElementSyntax childXmlElement
97+
&& childXmlElement.StartTag?.Name?.LocalName.ValueText == "li"
98+
&& childXmlElement.StartTag.Name.Prefix == null)
99+
{
100+
oldStartToken = childXmlElement.StartTag.Name.LocalName;
101+
newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, XmlCommentHelper.ItemXmlTag, oldStartToken.TrailingTrivia);
102+
var newChildXmlElement = childXmlElement.ReplaceToken(oldStartToken, newStartToken);
103+
104+
oldEndToken = newChildXmlElement.EndTag.Name.LocalName;
105+
newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, XmlCommentHelper.ItemXmlTag, oldEndToken.TrailingTrivia);
106+
newChildXmlElement = newChildXmlElement.ReplaceToken(oldEndToken, newEndToken);
107+
108+
newChildXmlElement = newChildXmlElement.WithContent(XmlSyntaxFactory.List(XmlSyntaxFactory.Element(XmlCommentHelper.DescriptionXmlTag, newChildXmlElement.Content)));
109+
110+
newXmlElement = newXmlElement.ReplaceNode(childXmlElement, newChildXmlElement);
111+
}
112+
}
113+
}
114+
115+
return document.WithSyntaxRoot(root.ReplaceNode(xmlElement, newXmlElement));
116+
}
117+
}
118+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Test.CSharp7.PortabilityRules
5+
{
6+
using DocumentationAnalyzers.Test.PortabilityRules;
7+
8+
public class DOC200CSharp7UnitTests : DOC200UnitTests
9+
{
10+
}
11+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Test.PortabilityRules
5+
{
6+
using System.Threading.Tasks;
7+
using DocumentationAnalyzers.PortabilityRules;
8+
using Xunit;
9+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.PortabilityRules.DOC200UseXmlDocumentationSyntax, DocumentationAnalyzers.PortabilityRules.DOC200CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
10+
11+
/// <summary>
12+
/// This class contains unit tests for <see cref="DOC200UseXmlDocumentationSyntax"/>.
13+
/// </summary>
14+
public class DOC200UnitTests
15+
{
16+
[Fact]
17+
public async Task TestHtmlParagraphAsync()
18+
{
19+
var testCode = @"
20+
/// <remarks>
21+
/// <[|p|]>This is a paragraph.</p>
22+
/// </remarks>
23+
class TestClass { }
24+
";
25+
var fixedCode = @"
26+
/// <remarks>
27+
/// <para>This is a paragraph.</para>
28+
/// </remarks>
29+
class TestClass { }
30+
";
31+
32+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
33+
}
34+
35+
[Fact]
36+
public async Task TestHtmlParagraphWithAttributeAsync()
37+
{
38+
var testCode = @"
39+
/// <remarks>
40+
/// <[|p|] attr=""value"">This is a paragraph.</p>
41+
/// </remarks>
42+
class TestClass { }
43+
";
44+
var fixedCode = @"
45+
/// <remarks>
46+
/// <para attr=""value"">This is a paragraph.</para>
47+
/// </remarks>
48+
class TestClass { }
49+
";
50+
51+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
52+
}
53+
54+
[Fact]
55+
public async Task TestPrefixAsync()
56+
{
57+
var testCode = @"
58+
/// <remarks>
59+
/// <not:p>This is a paragraph.</not:p>
60+
/// </remarks>
61+
class TestClass { }
62+
";
63+
64+
await Verify.VerifyAnalyzerAsync(testCode);
65+
}
66+
67+
[Fact]
68+
public async Task TestHtmlCodeAsync()
69+
{
70+
var testCode = @"
71+
/// <remarks>
72+
/// <para>This is <[|tt|]>code</tt>.</para>
73+
/// </remarks>
74+
class TestClass { }
75+
";
76+
var fixedCode = @"
77+
/// <remarks>
78+
/// <para>This is <c>code</c>.</para>
79+
/// </remarks>
80+
class TestClass { }
81+
";
82+
83+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
84+
}
85+
86+
[Fact]
87+
public async Task TestHtmlCodeBlockAsync()
88+
{
89+
var testCode = @"
90+
/// <remarks>
91+
/// <para>This is a code block:</para>
92+
/// <[|pre|]>
93+
/// code goes here
94+
/// more code here
95+
/// </pre>
96+
/// </remarks>
97+
class TestClass { }
98+
";
99+
var fixedCode = @"
100+
/// <remarks>
101+
/// <para>This is a code block:</para>
102+
/// <code>
103+
/// code goes here
104+
/// more code here
105+
/// </code>
106+
/// </remarks>
107+
class TestClass { }
108+
";
109+
110+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
111+
}
112+
113+
[Fact]
114+
public async Task TestHtmlOrderedListAsync()
115+
{
116+
var testCode = @"
117+
/// <remarks>
118+
/// <para>This is an ordered list:</para>
119+
/// <[|ol|]>
120+
/// <li>Item 1</li>
121+
/// <li>Item 2</li>
122+
/// </ol>
123+
/// </remarks>
124+
class TestClass { }
125+
";
126+
var fixedCode = @"
127+
/// <remarks>
128+
/// <para>This is an ordered list:</para>
129+
/// <list type=""number"">
130+
/// <item><description>Item 1</description></item>
131+
/// <item><description>Item 2</description></item>
132+
/// </list>
133+
/// </remarks>
134+
class TestClass { }
135+
";
136+
137+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
138+
}
139+
140+
[Fact]
141+
public async Task TestHtmlUnorderedListAsync()
142+
{
143+
var testCode = @"
144+
/// <remarks>
145+
/// <para>This is an ordered list:</para>
146+
/// <[|ul|]>
147+
/// <li>Item 1</li>
148+
/// <li>Item 2</li>
149+
/// </ul>
150+
/// </remarks>
151+
class TestClass { }
152+
";
153+
var fixedCode = @"
154+
/// <remarks>
155+
/// <para>This is an ordered list:</para>
156+
/// <list type=""bullet"">
157+
/// <item><description>Item 1</description></item>
158+
/// <item><description>Item 2</description></item>
159+
/// </list>
160+
/// </remarks>
161+
class TestClass { }
162+
";
163+
164+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
165+
}
166+
167+
[Fact]
168+
public async Task TestHtmlUnorderedListMultilineItemAsync()
169+
{
170+
var testCode = @"
171+
/// <remarks>
172+
/// <para>This is an ordered list:</para>
173+
/// <[|ul|]>
174+
/// <li>
175+
/// Item 1
176+
/// </li>
177+
/// </ul>
178+
/// </remarks>
179+
class TestClass { }
180+
";
181+
var fixedCode = @"
182+
/// <remarks>
183+
/// <para>This is an ordered list:</para>
184+
/// <list type=""bullet"">
185+
/// <item><description>
186+
/// Item 1
187+
/// </description></item>
188+
/// </list>
189+
/// </remarks>
190+
class TestClass { }
191+
";
192+
193+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
194+
}
195+
}
196+
}

DocumentationAnalyzers/DocumentationAnalyzers/AnalyzerCategory.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ internal static class AnalyzerCategory
1313
/// </summary>
1414
public const string StyleRules = nameof(DocumentationAnalyzers) + "." + nameof(StyleRules);
1515

16+
/// <summary>
17+
/// Category definition for portability rules.
18+
/// </summary>
19+
public const string PortabilityRules = nameof(DocumentationAnalyzers) + "." + nameof(PortabilityRules);
20+
1621
/// <summary>
1722
/// Category definition for refactorings.
1823
/// </summary>

DocumentationAnalyzers/DocumentationAnalyzers/Helpers/XmlCommentHelper.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ internal static class XmlCommentHelper
2424
internal const string SeeXmlTag = "see";
2525
internal const string CodeXmlTag = "code";
2626
internal const string ListXmlTag = "list";
27+
internal const string ItemXmlTag = "item";
28+
internal const string TermXmlTag = "term";
29+
internal const string DescriptionXmlTag = "description";
2730
internal const string NoteXmlTag = "note";
2831
internal const string ParaXmlTag = "para";
2932
internal const string SeeAlsoXmlTag = "seealso";
@@ -40,6 +43,7 @@ internal static class XmlCommentHelper
4043
internal const string PathAttributeName = "path";
4144
internal const string CrefArgumentName = "cref";
4245
internal const string NameArgumentName = "name";
46+
internal const string TypeAttributeName = "type";
4347

4448
/// <summary>
4549
/// The &lt;placeholder&gt; tag is a Sandcastle Help File Builder extension to the standard XML documentation

0 commit comments

Comments
 (0)