diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs new file mode 100644 index 0000000..01dd849 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs @@ -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 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.BlockLevelDocumentationCodeFix, + token => GetTransformedDocumentAsync(context.Document, diagnostic, token), + nameof(DOC103CodeFixProvider)), + diagnostic); + } + + return SpecializedTasks.CompletedTask; + } + + private static async Task 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(); + 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
  • ...
  • element with ... + 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)); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/StyleRules/DOC103CSharp7UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/StyleRules/DOC103CSharp7UnitTests.cs new file mode 100644 index 0000000..e826ff9 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/StyleRules/DOC103CSharp7UnitTests.cs @@ -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 + { + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test/StyleRules/DOC103UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test/StyleRules/DOC103UnitTests.cs new file mode 100644 index 0000000..6b4a74a --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test/StyleRules/DOC103UnitTests.cs @@ -0,0 +1,201 @@ +// 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.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using DocumentationAnalyzers.StyleRules; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Testing; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier; + + /// + /// This class contains unit tests for . + /// + public class DOC103UnitTests + { + [Fact] + public async Task TestHtmlParagraphAsync() + { + var testCode = @" +/// +/// <[|p|]>This is a paragraph.

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

    +///
    +class TestClass { } +"; + var fixedCode = @" +/// +/// This is a paragraph. +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestPrefixAsync() + { + var testCode = @" +/// +/// This is a paragraph. +/// +class TestClass { } +"; + + await Verify.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task TestHtmlCodeAsync() + { + var testCode = @" +/// +/// This is <[|tt|]>code. +/// +class TestClass { } +"; + var fixedCode = @" +/// +/// This is code. +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestHtmlCodeBlockAsync() + { + var testCode = @" +/// +/// This is a code block: +/// <[|pre|]> +/// code goes here +/// more code here +/// +/// +class TestClass { } +"; + var fixedCode = @" +/// +/// This is a code block: +/// +/// code goes here +/// more code here +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestHtmlOrderedListAsync() + { + var testCode = @" +/// +/// This is an ordered list: +/// <[|ol|]> +///
  • Item 1
  • +///
  • Item 2
  • +/// +///
    +class TestClass { } +"; + var fixedCode = @" +/// +/// This is an ordered list: +/// +/// Item 1 +/// Item 2 +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestHtmlUnorderedListAsync() + { + var testCode = @" +/// +/// This is an ordered list: +/// <[|ul|]> +///
  • Item 1
  • +///
  • Item 2
  • +/// +///
    +class TestClass { } +"; + var fixedCode = @" +/// +/// This is an ordered list: +/// +/// Item 1 +/// Item 2 +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestHtmlUnorderedListMultilineItemAsync() + { + var testCode = @" +/// +/// This is an ordered list: +/// <[|ul|]> +///
  • +/// Item 1 +///
  • +/// +///
    +class TestClass { } +"; + var fixedCode = @" +/// +/// This is an ordered list: +/// +/// +/// Item 1 +/// +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/Helpers/XmlCommentHelper.cs b/DocumentationAnalyzers/DocumentationAnalyzers/Helpers/XmlCommentHelper.cs index 8e86abe..61177a3 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/Helpers/XmlCommentHelper.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/Helpers/XmlCommentHelper.cs @@ -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"; @@ -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"; /// /// The <placeholder> tag is a Sandcastle Help File Builder extension to the standard XML documentation diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseXmlDocumentationSyntax.cs b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseXmlDocumentationSyntax.cs new file mode 100644 index 0000000..ce68a53 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseXmlDocumentationSyntax.cs @@ -0,0 +1,68 @@ +// 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 Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + /// + /// Use XML documentation syntax. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class DOC103UseXmlDocumentationSyntax : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "DOC103"; + private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC103.md"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(StyleResources.DOC103Title), StyleResources.ResourceManager, typeof(StyleResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(StyleResources.DOC103MessageFormat), StyleResources.ResourceManager, typeof(StyleResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(StyleResources.DOC103Description), StyleResources.ResourceManager, typeof(StyleResources)); + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.StyleRules, DiagnosticSeverity.Info, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + /// + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(HandleXmlElementSyntax, SyntaxKind.XmlElement); + } + + private static void HandleXmlElementSyntax(SyntaxNodeAnalysisContext context) + { + var xmlElementSyntax = (XmlElementSyntax)context.Node; + var name = xmlElementSyntax.StartTag?.Name; + if (name is null || name.Prefix != null) + { + return; + } + + switch (name.LocalName.ValueText) + { + case "p": + case "pre": + case "tt": + case "ol": + case "ul": + break; + + default: + return; + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, name.LocalName.GetLocation())); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs index 046a30c..fcca0d5 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs @@ -150,5 +150,32 @@ internal static string DOC102Title { return ResourceManager.GetString("DOC102Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Use XML documentation syntax. + /// + internal static string DOC103Description { + get { + return ResourceManager.GetString("DOC103Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use XML documentation syntax. + /// + internal static string DOC103MessageFormat { + get { + return ResourceManager.GetString("DOC103MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use XML documentation syntax. + /// + internal static string DOC103Title { + get { + return ResourceManager.GetString("DOC103Title", resourceCulture); + } + } } } diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx index 2680f3e..9388bf7 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx @@ -147,4 +147,13 @@ Use child blocks consistently across elements of the same kind + + Use XML documentation syntax + + + Use XML documentation syntax + + + Use XML documentation syntax + \ No newline at end of file diff --git a/docs/DOC103.md b/docs/DOC103.md new file mode 100644 index 0000000..7b50e2b --- /dev/null +++ b/docs/DOC103.md @@ -0,0 +1,50 @@ +# DOC103 + + + + + + + + + + + + + + +
    TypeNameDOC103UseXmlDocumentationSyntax
    CheckIdDOC103
    CategoryStyle Rules
    + +## Cause + +The documentation for the element an HTML element equivalent to a known XML documentation element. + +## Rule description + +A violation of this rule occurs when an XML documentation comment contains an HTML element instead of the corresponding +XML documentation comment syntax. + +## How to fix violations + +To fix a violation of this rule, use the expected XML documentation element instead of the HTML element. + +| HTML Element | XML Element | +| --- | --- | +| `

    ` | `` | +| `` | `` | +| `

    ` | `` |
    +| `
      ` | `` | +| `
        ` | `` | + +## How to suppress violations + +```csharp +#pragma warning disable DOC103 // Use XML documentation syntax +/// +/// Summary text with inline code. +/// +public void SomeOperation() +#pragma warning restore DOC103 // Use XML documentation syntax +{ +} +``` diff --git a/docs/StyleRules.md b/docs/StyleRules.md index ca7cbd7..22e3c52 100644 --- a/docs/StyleRules.md +++ b/docs/StyleRules.md @@ -7,3 +7,4 @@ Identifier | Name | Description [DOC100](DOC100.md) | PlaceTextInParagraphs | A `` or `` documentation element contains content which is not wrapped in a block-level element. [DOC101](DOC101.md) | UseChildBlocksConsistently | The documentation for the element contains some text which is wrapped in block-level elements, and other text which is written inline. [DOC102](DOC102.md) | UseChildBlocksConsistentlyAcrossElementsOfTheSameKind | The documentation for the element contains inline text, but the documentation for a sibling element of the same kind uses block-level elements. +[DOC103](DOC103.md) | UseXmlDocumentationSyntax | The documentation for the element an HTML element equivalent to a known XML documentation element.