diff --git a/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs new file mode 100644 index 0000000..13e9b78 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.CodeFixes/StyleRules/DOC103CodeFixProvider.cs @@ -0,0 +1,77 @@ +// 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.Diagnostics; + using System.Net; + 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; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC103CodeFixProvider))] + [Shared] + internal class DOC103CodeFixProvider : CodeFixProvider + { + private const string CS1570 = nameof(CS1570); + + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(DOC103UseUnicodeCharacters.DiagnosticId, CS1570); + + public override FixAllProvider GetFixAllProvider() + => CustomFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + foreach (var diagnostic in context.Diagnostics) + { + Debug.Assert(FixableDiagnosticIds.Contains(diagnostic.Id), "Assertion failed: FixableDiagnosticIds.Contains(diagnostic.Id)"); + + SyntaxToken token = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true); + if (!token.IsKind(SyntaxKind.XmlEntityLiteralToken)) + { + // Could be an unrelated CS1570 error. + return; + } + + string newText = token.ValueText; + if (newText == token.Text) + { + // The entity is not recognized. Try decoding as an HTML entity. + newText = WebUtility.HtmlDecode(token.Text); + } + + if (newText == token.Text) + { + // Unknown entity + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + StyleResources.DOC103CodeFix, + cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic, newText, cancellationToken), + nameof(DOC103CodeFixProvider)), + diagnostic); + } + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, string newText, CancellationToken cancellationToken) + { + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + SyntaxToken token = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true); + + var newToken = SyntaxFactory.Token(token.LeadingTrivia, SyntaxKind.XmlTextLiteralToken, newText, newText, token.TrailingTrivia); + + return document.WithSyntaxRoot(root.ReplaceToken(token, newToken)); + } + } +} 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..47603d5 --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers.Test/StyleRules/DOC103UnitTests.cs @@ -0,0 +1,243 @@ +// 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 Microsoft.CodeAnalysis.CSharp.Testing; + using Microsoft.CodeAnalysis.Testing; + using Microsoft.CodeAnalysis.Testing.Verifiers; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier; + + public class DOC103UnitTests + { + [Fact] + public async Task TestApostropheReplacementAsync() + { + var testCode = @" +/// +/// Don[|'|]t use this . +/// +class TestClass +{ +} +"; + var fixedCode = @" +/// +/// Don't use this . +/// +class TestClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestApostropheReplacementByNumberAsync() + { + var testCode = @" +/// +/// Don[|'|]t use this . +/// +class TestClass +{ +} +"; + var fixedCode = @" +/// +/// Don't use this . +/// +class TestClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestQuoteReplacementAsync() + { + var testCode = @" +/// +/// Don[|"|]t use this . +/// +class TestClass +{ +} +"; + var fixedCode = @" +/// +/// Don""t use this . +/// +class TestClass +{ +} +"; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestHtmlEntityReplacementAsync() + { + var testCode = @" +/// +/// From A→B. +/// +class TestClass +{ +} +"; + var fixedCode = @" +/// +/// From A→B. +/// +class TestClass +{ +} +"; + + await new CSharpCodeFixTest + { + TestCode = testCode, + ExpectedDiagnostics = { DiagnosticResult.CompilerWarning("CS1570").WithSpan(3, 11, 3, 11).WithMessage("XML comment has badly formed XML -- 'Reference to undefined entity 'rarr'.'") }, + FixedCode = fixedCode, + CompilerDiagnostics = CompilerDiagnostics.Warnings, + }.RunAsync(); + } + + [Fact] + public async Task TestUnknownEntityNotReplacedAsync() + { + var testCode = @" +/// +/// Unknown entity &myEntity;. +/// +class TestClass +{ +} +"; + var fixedCode = testCode; + + await new CSharpCodeFixTest + { + TestState = + { + Sources = { testCode }, + ExpectedDiagnostics = { DiagnosticResult.CompilerWarning("CS1570").WithSpan(3, 20, 3, 20).WithMessage("XML comment has badly formed XML -- 'Reference to undefined entity 'myEntity'.'") }, + }, + FixedState = + { + Sources = { fixedCode }, + InheritanceMode = StateInheritanceMode.AutoInheritAll, + }, + CompilerDiagnostics = CompilerDiagnostics.Warnings, + }.RunAsync(); + } + + [Fact] + public async Task TestHtmlEntityReplacementInInvalidXmlAsync() + { + var testCode = @" +/// +/// From A→B. +///

+/// An unterminated second paragraph... +///

+class TestClass +{ +} +"; + var fixedCode = @" +/// +/// From A→B. +///

+/// An unterminated second paragraph... +///

+class TestClass +{ +} +"; + + await new CSharpCodeFixTest + { + TestState = + { + Sources = { testCode }, + ExpectedDiagnostics = + { + DiagnosticResult.CompilerWarning("CS1570").WithSpan(3, 11, 3, 11).WithMessage("XML comment has badly formed XML -- 'Reference to undefined entity 'rarr'.'"), + DiagnosticResult.CompilerWarning("CS1570").WithSpan(6, 7, 6, 14).WithMessage("XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'p'.'"), + DiagnosticResult.CompilerWarning("CS1570").WithSpan(7, 1, 7, 1).WithMessage("XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'"), + }, + }, + FixedState = + { + Sources = { fixedCode }, + ExpectedDiagnostics = + { + DiagnosticResult.CompilerWarning("CS1570").WithSpan(6, 7, 6, 14).WithMessage("XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'p'.'"), + DiagnosticResult.CompilerWarning("CS1570").WithSpan(7, 1, 7, 1).WithMessage("XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'"), + }, + }, + CompilerDiagnostics = CompilerDiagnostics.Warnings, + }.RunAsync(); + } + + [Fact] + public async Task TestNoCodeFixForRequiredEntityAsync() + { + var testCode = @" +/// +/// Processing for <code> elements. +/// +class TestClass +{ +} +"; + var fixedCode = testCode; + + await Verify.VerifyCodeFixAsync(testCode, fixedCode); + } + + [Fact] + public async Task TestNoCodeFixForInvalidXmlAsync() + { + var testCode = @" +/// +/// From A to B. +///

+/// An unterminated second paragraph... +///

+class TestClass +{ +} +"; + var fixedCode = testCode; + + await new CSharpCodeFixTest + { + TestState = + { + Sources = { testCode }, + ExpectedDiagnostics = + { + DiagnosticResult.CompilerWarning("CS1570").WithSpan(6, 7, 6, 14).WithMessage("XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'p'.'"), + DiagnosticResult.CompilerWarning("CS1570").WithSpan(7, 1, 7, 1).WithMessage("XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'"), + }, + }, + FixedState = + { + Sources = { fixedCode }, + InheritanceMode = StateInheritanceMode.AutoInheritAll, + }, + CompilerDiagnostics = CompilerDiagnostics.Warnings, + }.RunAsync(); + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseUnicodeCharacters.cs b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseUnicodeCharacters.cs new file mode 100644 index 0000000..10e993f --- /dev/null +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/DOC103UseUnicodeCharacters.cs @@ -0,0 +1,67 @@ +// 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 Unicode characters. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class DOC103UseUnicodeCharacters : 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.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.XmlText); + } + + private static void HandleXmlElementSyntax(SyntaxNodeAnalysisContext context) + { + var xmlText = (XmlTextSyntax)context.Node; + foreach (var token in xmlText.TextTokens) + { + if (!token.IsKind(SyntaxKind.XmlEntityLiteralToken)) + { + continue; + } + + switch (token.ValueText) + { + // Characters which are often XML-escaped unnecessarily + case "'": + case "\"": + context.ReportDiagnostic(Diagnostic.Create(Descriptor, token.GetLocation())); + break; + + default: + continue; + } + } + } + } +} diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs index 15e16a8..bdd33ea 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs @@ -151,6 +151,42 @@ internal static string DOC102Title { } } + /// + /// Looks up a localized string similar to Use Unicode characters. + /// + internal static string DOC103CodeFix { + get { + return ResourceManager.GetString("DOC103CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use Unicode characters. + /// + internal static string DOC103Description { + get { + return ResourceManager.GetString("DOC103Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use Unicode characters. + /// + internal static string DOC103MessageFormat { + get { + return ResourceManager.GetString("DOC103MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use Unicode characters. + /// + internal static string DOC103Title { + get { + return ResourceManager.GetString("DOC103Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Wrap text in paragraph element. /// diff --git a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx index e8476b5..cd7c46d 100644 --- a/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx +++ b/DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx @@ -147,6 +147,18 @@ Use child blocks consistently across elements of the same kind + + Use Unicode characters + + + Use Unicode characters + + + Use Unicode characters + + + Use Unicode characters + Wrap text in paragraph element diff --git a/docs/DOC103.md b/docs/DOC103.md new file mode 100644 index 0000000..6b9b6ba --- /dev/null +++ b/docs/DOC103.md @@ -0,0 +1,61 @@ +# DOC103 + + + + + + + + + + + + + + +
TypeNameDOC103UseUnicodeCharacters
CheckIdDOC103
CategoryStyle Rules
+ +## Cause + +The documentation contains an unnecessary or unrecognized HTML character entity. + +## Rule description + +A violation of this rule occurs when documentation an unnecessarily-escaped character, making the documentation more +difficult to read than necessary. The code fix for this diagnostic also helps correct HTML character entity references +(e.g. `&rarr`), which are not supported by the XML documentation compiler. + +```csharp +/// +/// A city's graph from A→B. +/// +public class SomeType +{ +} +``` + +## How to fix violations + +To fix a violation of this rule, replace the XML- or HTML-escape sequence with the intended character. + +```csharp +/// +/// A city's graph from A→B. +/// +public class SomeType +{ +} +``` + +## How to suppress violations + +```csharp +#pragma warning disable DOC103 // Use Unicode characters +/// +/// A city's graph from A→B. +/// +public class SomeType +#pragma warning restore DOC103 // Use Unicode characters +{ +} +``` diff --git a/docs/StyleRules.md b/docs/StyleRules.md index 55856fa..5fea148 100644 --- a/docs/StyleRules.md +++ b/docs/StyleRules.md @@ -7,4 +7,5 @@ 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) | UseUnicodeCharacters | The documentation contains an unnecessary or unrecognized HTML character entity. [DOC108](DOC108.md) | AvoidEmptyParagraphs | The documentation contains an empty paragraph element (`` or `

`) used as a paragraph separator.