Skip to content

Commit

Permalink
Implement DOC103 (Use Unicode Characters)
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Sep 21, 2018
1 parent bfe856e commit ea7c1a2
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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.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<string> FixableDiagnosticIds { get; }
= ImmutableArray.Create(DOC103UseUnicodeCharacters.DiagnosticId, CS1570);

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);

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
return document;
}

var newToken = SyntaxFactory.Token(token.LeadingTrivia, SyntaxKind.XmlTextLiteralToken, newText, newText, token.TrailingTrivia);

return document.WithSyntaxRoot(root.ReplaceToken(token, newToken));
}
}
}
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,114 @@
// 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<DocumentationAnalyzers.StyleRules.DOC103UseUnicodeCharacters, DocumentationAnalyzers.StyleRules.DOC103CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;

public class DOC103UnitTests
{
[Fact]
public async Task TestApostropheReplacementAsync()
{
var testCode = @"
/// <summary>
/// Don[|&apos;|]t use <![CDATA[&apos;]]> this <element attr=""&apos;"" attr2='&apos;'/>.
/// </summary>
class TestClass
{
}
";
var fixedCode = @"
/// <summary>
/// Don't use <![CDATA[&apos;]]> this <element attr=""&apos;"" attr2='&apos;'/>.
/// </summary>
class TestClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestApostropheReplacementByNumberAsync()
{
var testCode = @"
/// <summary>
/// Don[|&#39;|]t use <![CDATA[&#39;]]> this <element attr=""&#39;"" attr2='&#39;'/>.
/// </summary>
class TestClass
{
}
";
var fixedCode = @"
/// <summary>
/// Don't use <![CDATA[&#39;]]> this <element attr=""&#39;"" attr2='&#39;'/>.
/// </summary>
class TestClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestQuoteReplacementAsync()
{
var testCode = @"
/// <summary>
/// Don[|&quot;|]t use <![CDATA[&quot;]]> this <element attr=""&quot;"" attr2='&quot;'/>.
/// </summary>
class TestClass
{
}
";
var fixedCode = @"
/// <summary>
/// Don""t use <![CDATA[&quot;]]> this <element attr=""&quot;"" attr2='&quot;'/>.
/// </summary>
class TestClass
{
}
";

await Verify.VerifyCodeFixAsync(testCode, fixedCode);
}

[Fact]
public async Task TestHtmlEntityReplacementAsync()
{
var testCode = @"
/// <summary>
/// From A&rarr;B.
/// </summary>
class TestClass
{
}
";
var fixedCode = @"
/// <summary>
/// From A→B.
/// </summary>
class TestClass
{
}
";

await new CSharpCodeFixTest<DOC103UseUnicodeCharacters, DOC103CodeFixProvider, XUnitVerifier>
{
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Use Unicode characters <see text="&quot;"/>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class DOC103UseUnicodeCharacters : DiagnosticAnalyzer
{
/// <summary>
/// The ID for diagnostics produced by the <see cref="DOC103UseUnicodeCharacters"/> analyzer.
/// </summary>
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);

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> 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;
}
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,16 @@
<data name="DOC102Title" xml:space="preserve">
<value>Use child blocks consistently across elements of the same kind</value>
</data>
<data name="DOC103CodeFix" xml:space="preserve">
<value>Use Unicode characters</value>
</data>
<data name="DOC103Description" xml:space="preserve">
<value>Use Unicode characters</value>
</data>
<data name="DOC103MessageFormat" xml:space="preserve">
<value>Use Unicode characters</value>
</data>
<data name="DOC103Title" xml:space="preserve">
<value>Use Unicode characters</value>
</data>
</root>
61 changes: 61 additions & 0 deletions docs/DOC103.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# DOC103

<table>
<tr>
<td>TypeName</td>
<td>DOC103UseUnicodeCharacters</td>
</tr>
<tr>
<td>CheckId</td>
<td>DOC103</td>
</tr>
<tr>
<td>Category</td>
<td>Style Rules</td>
</tr>
</table>

## 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
/// <summary>
/// A city&apos;s graph from A&rarr;B.
/// </summary>
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
/// <summary>
/// A city's graph from A→B.
/// </summary>
public class SomeType
{
}
```

## How to suppress violations

```csharp
#pragma warning disable DOC103 // Use Unicode characters
/// <summary>
/// A city&apos;s graph from A&rarr;B.
/// </summary>
public class SomeType
#pragma warning restore DOC103 // Use Unicode characters
{
}
```
1 change: 1 addition & 0 deletions docs/StyleRules.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Identifier | Name | Description
[DOC100](DOC100.md) | PlaceTextInParagraphs | A `<remarks>` or `<note>` 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.

0 comments on commit ea7c1a2

Please sign in to comment.