Skip to content

Commit

Permalink
Implement DOC103 (Use XML Documentation Syntax)
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Sep 17, 2018
1 parent 266f47b commit 5e0e9be
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT license. See LICENSE in the project root for license information.

namespace DocumentationAnalyzers.StyleRules
{
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using DocumentationAnalyzers.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC103CodeFixProvider))]
[Shared]
internal class DOC103CodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; }
= ImmutableArray.Create(DOC103UseXmlDocumentationSyntax.DiagnosticId);

public override FixAllProvider GetFixAllProvider()
=> CustomFixAllProviders.BatchFixer;

public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
foreach (var diagnostic in context.Diagnostics)
{
if (!FixableDiagnosticIds.Contains(diagnostic.Id))
{
continue;
}

context.RegisterCodeFix(
CodeAction.Create(
StyleResources.DOC103CodeFix,
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
nameof(DOC103CodeFixProvider)),
diagnostic);
}

return SpecializedTasks.CompletedTask;
}

private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxToken token = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true);

var xmlElement = token.Parent.FirstAncestorOrSelf<XmlElementSyntax>();
var oldStartToken = xmlElement.StartTag.Name.LocalName;

string newIdentifier;
switch (oldStartToken.ValueText)
{
case "p":
newIdentifier = XmlCommentHelper.ParaXmlTag;
break;

case "tt":
newIdentifier = XmlCommentHelper.CXmlTag;
break;

case "pre":
newIdentifier = XmlCommentHelper.CodeXmlTag;
break;

case "ul":
case "ol":
newIdentifier = XmlCommentHelper.ListXmlTag;
break;

default:
// Not handled
return document;
}

var newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, newIdentifier, oldStartToken.TrailingTrivia);
var newXmlElement = xmlElement.ReplaceToken(oldStartToken, newStartToken);

var oldEndToken = newXmlElement.EndTag.Name.LocalName;
var newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, newIdentifier, oldEndToken.TrailingTrivia);
newXmlElement = newXmlElement.ReplaceToken(oldEndToken, newEndToken);

if (newIdentifier == XmlCommentHelper.ListXmlTag)
{
// Add an attribute for the list kind
string listType = oldStartToken.ValueText == "ol" ? "number" : "bullet";
newXmlElement = newXmlElement.WithStartTag(newXmlElement.StartTag.AddAttributes(XmlSyntaxFactory.TextAttribute(XmlCommentHelper.TypeAttributeName, listType)));

// Replace each <li>...</li> element with <item><description>...</description></item>
for (int i = 0; i < newXmlElement.Content.Count; i++)
{
if (newXmlElement.Content[i] is XmlElementSyntax childXmlElement
&& childXmlElement.StartTag?.Name?.LocalName.ValueText == "li"
&& childXmlElement.StartTag.Name.Prefix == null)
{
oldStartToken = childXmlElement.StartTag.Name.LocalName;
newStartToken = SyntaxFactory.Identifier(oldStartToken.LeadingTrivia, XmlCommentHelper.ItemXmlTag, oldStartToken.TrailingTrivia);
var newChildXmlElement = childXmlElement.ReplaceToken(oldStartToken, newStartToken);

oldEndToken = newChildXmlElement.EndTag.Name.LocalName;
newEndToken = SyntaxFactory.Identifier(oldEndToken.LeadingTrivia, XmlCommentHelper.ItemXmlTag, oldEndToken.TrailingTrivia);
newChildXmlElement = newChildXmlElement.ReplaceToken(oldEndToken, newEndToken);

newChildXmlElement = newChildXmlElement.WithContent(XmlSyntaxFactory.List(XmlSyntaxFactory.Element(XmlCommentHelper.DescriptionXmlTag, newChildXmlElement.Content)));

newXmlElement = newXmlElement.ReplaceNode(childXmlElement, newChildXmlElement);
}
}
}

return document.WithSyntaxRoot(root.ReplaceNode(xmlElement, newXmlElement));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT license. See LICENSE in the project root for license information.

namespace DocumentationAnalyzers.Test.CSharp7.StyleRules
{
using DocumentationAnalyzers.Test.StyleRules;

public class DOC103CSharp7UnitTests : DOC103UnitTests
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
// Licensed under the MIT license. See LICENSE in the project root for license information.

namespace DocumentationAnalyzers.Test.StyleRules
{
using System.Threading.Tasks;
using DocumentationAnalyzers.StyleRules;
using Xunit;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.StyleRules.DOC103UseXmlDocumentationSyntax, DocumentationAnalyzers.StyleRules.DOC103CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;

/// <summary>
/// This class contains unit tests for <see cref="DOC103UseXmlDocumentationSyntax"/>.
/// </summary>
public class DOC103UnitTests
{
[Fact]
public async Task TestHtmlParagraphAsync()
{
var testCode = @"
/// <remarks>
/// <[|p|]>This is a paragraph.</p>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is a paragraph.</para>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlParagraphWithAttributeAsync()
{
var testCode = @"
/// <remarks>
/// <[|p|] attr=""value"">This is a paragraph.</p>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para attr=""value"">This is a paragraph.</para>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestPrefixAsync()
{
var testCode = @"
/// <remarks>
/// <not:p>This is a paragraph.</not:p>
/// </remarks>
class TestClass { }
";

await Verify.VerifyAnalyzerAsync(testCode);
}

[Fact]
public async Task TestHtmlCodeAsync()
{
var testCode = @"
/// <remarks>
/// <para>This is <[|tt|]>code</tt>.</para>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is <c>code</c>.</para>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlCodeBlockAsync()
{
var testCode = @"
/// <remarks>
/// <para>This is a code block:</para>
/// <[|pre|]>
/// code goes here
/// more code here
/// </pre>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is a code block:</para>
/// <code>
/// code goes here
/// more code here
/// </code>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlOrderedListAsync()
{
var testCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <[|ol|]>
/// <li>Item 1</li>
/// <li>Item 2</li>
/// </ol>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <list type=""number"">
/// <item><description>Item 1</description></item>
/// <item><description>Item 2</description></item>
/// </list>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlUnorderedListAsync()
{
var testCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <[|ul|]>
/// <li>Item 1</li>
/// <li>Item 2</li>
/// </ul>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <list type=""bullet"">
/// <item><description>Item 1</description></item>
/// <item><description>Item 2</description></item>
/// </list>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlUnorderedListMultilineItemAsync()
{
var testCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <[|ul|]>
/// <li>
/// Item 1
/// </li>
/// </ul>
/// </remarks>
class TestClass { }
";
var fixedCode = @"
/// <remarks>
/// <para>This is an ordered list:</para>
/// <list type=""bullet"">
/// <item><description>
/// Item 1
/// </description></item>
/// </list>
/// </remarks>
class TestClass { }
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ internal static class XmlCommentHelper
internal const string SeeXmlTag = "see";
internal const string CodeXmlTag = "code";
internal const string ListXmlTag = "list";
internal const string ItemXmlTag = "item";
internal const string TermXmlTag = "term";
internal const string DescriptionXmlTag = "description";
internal const string NoteXmlTag = "note";
internal const string ParaXmlTag = "para";
internal const string SeeAlsoXmlTag = "seealso";
Expand All @@ -40,6 +43,7 @@ internal static class XmlCommentHelper
internal const string PathAttributeName = "path";
internal const string CrefArgumentName = "cref";
internal const string NameArgumentName = "name";
internal const string TypeAttributeName = "type";

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

0 comments on commit 5e0e9be

Please sign in to comment.