diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC201CodeFixProvider.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC201CodeFixProvider.cs new file mode 100644 index 0000000..90728e2 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/PortabilityRules/DOC201CodeFixProvider.cs @@ -0,0 +1,56 @@ +// 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.PortabilityRules +{ + 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.Syntax; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC201CodeFixProvider))] + [Shared] + internal class DOC201CodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DOC201ItemShouldHaveDescription.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( + PortabilityResources.DOC201CodeFix, + token => GetTransformedDocumentAsync(context.Document, diagnostic, token), + nameof(DOC201CodeFixProvider)), + 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 newXmlElement = xmlElement.WithContent(XmlSyntaxFactory.List(XmlSyntaxFactory.Element(XmlCommentHelper.DescriptionXmlTag, xmlElement.Content))); + return document.WithSyntaxRoot(root.ReplaceNode(xmlElement, newXmlElement)); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC201CSharp7UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC201CSharp7UnitTests.cs new file mode 100644 index 0000000..22c7276 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test.CSharp7/PortabilityRules/DOC201CSharp7UnitTests.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.PortabilityRules +{ + using DocumentationAnalyzers.Test.PortabilityRules; + + public class DOC201CSharp7UnitTests : DOC201UnitTests + { + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC201UnitTests.cs b/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC201UnitTests.cs new file mode 100644 index 0000000..b295258 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test/PortabilityRules/DOC201UnitTests.cs @@ -0,0 +1,102 @@ +// 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.PortabilityRules +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier; + + public class DOC201UnitTests + { + [Fact] + public async Task TestListItemsWithoutDescriptionAsync() + { + var testCode = @" +/// +/// +/// <[|item|]>Item 1 +/// <[|item|]>Item 2 +/// +/// +class TestClass { } +"; + var fixedCode = @" +/// +/// +/// Item 1 +/// Item 2 +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestMultilineListItemsWithoutDescriptionAsync() + { + var testCode = @" +/// +/// +/// <[|item|]> +/// Item 1 +/// +/// <[|item|]> +/// Item 2 +/// +/// +/// +class TestClass { } +"; + var fixedCode = @" +/// +/// +/// +/// Item 1 +/// +/// +/// Item 2 +/// +/// +/// +class TestClass { } +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestListItemsWithEmptyDescriptionAsync() + { + var testCode = @" +/// +/// +/// +/// +/// +/// +class TestClass { } +"; + + await Verify.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task TestListItemsWithTermAsync() + { + var testCode = @" +/// +/// +/// Item 1 +/// Item 2Description +/// +/// +class TestClass { } +"; + + await Verify.VerifyAnalyzerAsync(testCode); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC201ItemShouldHaveDescription.cs b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC201ItemShouldHaveDescription.cs new file mode 100644 index 0000000..b9be113 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/DOC201ItemShouldHaveDescription.cs @@ -0,0 +1,83 @@ +// 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.PortabilityRules +{ + using System.Collections.Immutable; + using DocumentationAnalyzers.Helpers; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class DOC201ItemShouldHaveDescription : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "DOC201"; + private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC201.md"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(PortabilityResources.DOC201Title), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(PortabilityResources.DOC201MessageFormat), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(PortabilityResources.DOC201Description), PortabilityResources.ResourceManager, typeof(PortabilityResources)); + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.PortabilityRules, 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 XmlCommentHelper.ItemXmlTag: + break; + + default: + return; + } + + // check for a or child element + foreach (var node in xmlElementSyntax.Content) + { + var childName = node.GetName(); + if (childName is null || childName.Prefix != null) + { + continue; + } + + switch (childName.LocalName.ValueText) + { + case XmlCommentHelper.TermXmlTag: + case XmlCommentHelper.DescriptionXmlTag: + // Avoid analyzing that already has and/or + return; + + default: + break; + } + } + + context.ReportDiagnostic(Diagnostic.Create(Descriptor, name.LocalName.GetLocation())); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs index 3682be2..2d4dae3 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.Designer.cs @@ -96,5 +96,41 @@ internal static string DOC200Title { return ResourceManager.GetString("DOC200Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Item should have description. + /// + internal static string DOC201CodeFix { + get { + return ResourceManager.GetString("DOC201CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item should have description. + /// + internal static string DOC201Description { + get { + return ResourceManager.GetString("DOC201Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item should have description. + /// + internal static string DOC201MessageFormat { + get { + return ResourceManager.GetString("DOC201MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item should have description. + /// + internal static string DOC201Title { + get { + return ResourceManager.GetString("DOC201Title", resourceCulture); + } + } } } diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx index 0fde5d7..c33f490 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx +++ b/DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.resx @@ -129,4 +129,16 @@ Use XML documentation syntax + + Item should have description + + + Item should have description + + + Item should have description + + + Item should have description + \ No newline at end of file diff --git a/docs/DOC201.md b/docs/DOC201.md new file mode 100644 index 0000000..923fa3a --- /dev/null +++ b/docs/DOC201.md @@ -0,0 +1,49 @@ +# DOC201 + + + + + + + + + + + + + + +
TypeNameDOC201ItemShouldHaveDescription
CheckIdDOC201
CategoryPortability Rules
+ +## Cause + +The documentation for an `` within a `` did not include the required `` and/or `` +elements. + +## Rule description + +According to the C# language specification, the `` element within a documentation comment must have its content +wrapped in a `` element. Not all documentation processing tools support omitting the `` +element, so it should be included for consistent behavior. + +See [dotnet/csharplang#1765](https://github.com/dotnet/csharplang/issues/1765) for a language proposal to natively +support lists with the `` element removed. + +## How to fix violations + +To fix a violation of this rule, wrap the content of the `` element in a `` element. + +## How to suppress violations + +```csharp +#pragma warning disable DOC201 // Item should have description +/// +/// +/// This item has text not wrapped in a description element. +/// +/// +public void SomeOperation() +#pragma warning restore DOC201 // Item should have description +{ +} +``` diff --git a/docs/PortabilityRules.md b/docs/PortabilityRules.md index dc6dbe6..7369d18 100644 --- a/docs/PortabilityRules.md +++ b/docs/PortabilityRules.md @@ -5,3 +5,4 @@ Rules related to the portability of documentation comments. Identifier | Name | Description -----------|------|------------- [DOC200](DOC200.md) | UseXmlDocumentationSyntax | The documentation for the element an HTML element equivalent to a known XML documentation element. +[DOC201](DOC201.md) | ItemShouldHaveDescription | The documentation for an `` within a `` did not include the required `` and/or `` elements.