Skip to content

Commit ae01750

Browse files
committed
Implement DOC108 (Avoid Empty Paragraphs)
Closes #17
1 parent bfe856e commit ae01750

File tree

7 files changed

+354
-0
lines changed

7 files changed

+354
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.StyleRules
5+
{
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using DocumentationAnalyzers.Helpers;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.CSharp.Syntax;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC108CodeFixProvider))]
17+
[Shared]
18+
internal class DOC108CodeFixProvider : CodeFixProvider
19+
{
20+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
21+
= ImmutableArray.Create(DOC108AvoidEmptyParagraphs.DiagnosticId);
22+
23+
public override FixAllProvider GetFixAllProvider()
24+
=> CustomFixAllProviders.BatchFixer;
25+
26+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
27+
{
28+
foreach (var diagnostic in context.Diagnostics)
29+
{
30+
if (!FixableDiagnosticIds.Contains(diagnostic.Id))
31+
{
32+
continue;
33+
}
34+
35+
context.RegisterCodeFix(
36+
CodeAction.Create(
37+
StyleResources.DOC108CodeFix,
38+
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
39+
nameof(DOC108CodeFixProvider)),
40+
diagnostic);
41+
}
42+
43+
return SpecializedTasks.CompletedTask;
44+
}
45+
46+
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
47+
{
48+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
49+
var xmlEmptyElement = root.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true) as XmlEmptyElementSyntax;
50+
if (xmlEmptyElement is null)
51+
{
52+
return document;
53+
}
54+
55+
return document.WithSyntaxRoot(root.RemoveNode(xmlEmptyElement, SyntaxRemoveOptions.KeepExteriorTrivia));
56+
}
57+
}
58+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.Test.StyleRules
5+
{
6+
using System.Threading.Tasks;
7+
using DocumentationAnalyzers.StyleRules;
8+
using Xunit;
9+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.StyleRules.DOC108AvoidEmptyParagraphs, DocumentationAnalyzers.StyleRules.DOC108CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
10+
11+
/// <summary>
12+
/// This class contains unit tests for <see cref="DOC108AvoidEmptyParagraphs"/>.
13+
/// </summary>
14+
public class DOC108UnitTests
15+
{
16+
[Fact]
17+
public async Task TestEmptyParagraphElementSeparatesParagraphsAsync()
18+
{
19+
var testCode = @"
20+
/// <summary>
21+
/// Summary 1
22+
/// $$<para/>
23+
/// Summary 2
24+
/// </summary>
25+
class TestClass
26+
{
27+
}
28+
";
29+
var fixedCode = @"
30+
/// <summary>
31+
/// Summary 1
32+
///
33+
/// Summary 2
34+
/// </summary>
35+
class TestClass
36+
{
37+
}
38+
";
39+
40+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
41+
}
42+
43+
[Fact]
44+
public async Task TestEmptyHtmlParagraphElementSeparatesParagraphsAsync()
45+
{
46+
var testCode = @"
47+
/// <summary>
48+
/// Summary 1
49+
/// $$<p/>
50+
/// Summary 2
51+
/// </summary>
52+
class TestClass
53+
{
54+
}
55+
";
56+
var fixedCode = @"
57+
/// <summary>
58+
/// Summary 1
59+
///
60+
/// Summary 2
61+
/// </summary>
62+
class TestClass
63+
{
64+
}
65+
";
66+
67+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
68+
}
69+
70+
[Fact]
71+
public async Task TestEmptyParagraphBeforeFullParagraphAsync()
72+
{
73+
var testCode = @"
74+
/// <summary>
75+
/// Summary 1
76+
/// $$<para/>
77+
/// <para>Summary 2</para>
78+
/// </summary>
79+
class TestClass
80+
{
81+
}
82+
";
83+
var fixedCode = @"
84+
/// <summary>
85+
/// Summary 1
86+
///
87+
/// <para>Summary 2</para>
88+
/// </summary>
89+
class TestClass
90+
{
91+
}
92+
";
93+
94+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
95+
}
96+
97+
[Fact]
98+
public async Task TestEmptyHtmlParagraphBeforeFullHtmlParagraphAsync()
99+
{
100+
var testCode = @"
101+
/// <summary>
102+
/// Summary 1
103+
/// $$<p/>
104+
/// <p>Summary 2</p>
105+
/// </summary>
106+
class TestClass
107+
{
108+
}
109+
";
110+
var fixedCode = @"
111+
/// <summary>
112+
/// Summary 1
113+
///
114+
/// <p>Summary 2</p>
115+
/// </summary>
116+
class TestClass
117+
{
118+
}
119+
";
120+
121+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
122+
}
123+
}
124+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT license. See LICENSE in the project root for license information.
3+
4+
namespace DocumentationAnalyzers.StyleRules
5+
{
6+
using System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
/// <summary>
13+
/// Avoid empty paragraphs.
14+
/// </summary>
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
internal class DOC108AvoidEmptyParagraphs : DiagnosticAnalyzer
17+
{
18+
/// <summary>
19+
/// The ID for diagnostics produced by the <see cref="DOC108AvoidEmptyParagraphs"/> analyzer.
20+
/// </summary>
21+
public const string DiagnosticId = "DOC108";
22+
private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC108.md";
23+
24+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(StyleResources.DOC108Title), StyleResources.ResourceManager, typeof(StyleResources));
25+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(StyleResources.DOC108MessageFormat), StyleResources.ResourceManager, typeof(StyleResources));
26+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(StyleResources.DOC108Description), StyleResources.ResourceManager, typeof(StyleResources));
27+
28+
private static readonly DiagnosticDescriptor Descriptor =
29+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.PortabilityRules, DiagnosticSeverity.Info, AnalyzerConstants.EnabledByDefault, Description, HelpLink);
30+
31+
/// <inheritdoc/>
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
33+
= ImmutableArray.Create(Descriptor);
34+
35+
public override void Initialize(AnalysisContext context)
36+
{
37+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
38+
context.EnableConcurrentExecution();
39+
40+
context.RegisterSyntaxNodeAction(HandleXmlEmptyElementSyntax, SyntaxKind.XmlEmptyElement);
41+
}
42+
43+
private static void HandleXmlEmptyElementSyntax(SyntaxNodeAnalysisContext context)
44+
{
45+
var xmlEmptyElement = (XmlEmptyElementSyntax)context.Node;
46+
var name = xmlEmptyElement.Name;
47+
if (name is null || name.Prefix != null)
48+
{
49+
return;
50+
}
51+
52+
switch (name.LocalName.ValueText)
53+
{
54+
case "para":
55+
case "p":
56+
break;
57+
58+
default:
59+
return;
60+
}
61+
62+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, xmlEmptyElement.GetLocation()));
63+
}
64+
}
65+
}

DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.Designer.cs

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DocumentationAnalyzers/DocumentationAnalyzers/StyleRules/StyleResources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,16 @@
147147
<data name="DOC102Title" xml:space="preserve">
148148
<value>Use child blocks consistently across elements of the same kind</value>
149149
</data>
150+
<data name="DOC108CodeFix" xml:space="preserve">
151+
<value>Wrap text in paragraph element</value>
152+
</data>
153+
<data name="DOC108Description" xml:space="preserve">
154+
<value>Avoid empty paragraphs</value>
155+
</data>
156+
<data name="DOC108MessageFormat" xml:space="preserve">
157+
<value>Avoid empty paragraphs</value>
158+
</data>
159+
<data name="DOC108Title" xml:space="preserve">
160+
<value>Avoid empty paragraphs</value>
161+
</data>
150162
</root>

docs/DOC108.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# DOC108
2+
3+
<table>
4+
<tr>
5+
<td>TypeName</td>
6+
<td>DOC108AvoidEmptyParagraphs</td>
7+
</tr>
8+
<tr>
9+
<td>CheckId</td>
10+
<td>DOC108</td>
11+
</tr>
12+
<tr>
13+
<td>Category</td>
14+
<td>Style Rules</td>
15+
</tr>
16+
</table>
17+
18+
## Cause
19+
20+
The documentation contains an empty paragraph element (`<para/>` or `<p/>`) used as a paragraph separator.
21+
22+
## Rule description
23+
24+
A violation of this rule occurs when a `<para/>` or `<p/>` is used as a paragraph separator. Rather than place an empty
25+
paragraph element between paragraphs, the text content of paragraphs should be contained in the `<para>` element.
26+
27+
```csharp
28+
/// <summary>Summary text.</summary>
29+
/// <remarks>
30+
/// Remarks text.
31+
/// <para/>
32+
/// Second paragraph of remarks, which is not placed inside the &lt;para&gt; element.
33+
/// </remarks>
34+
public void SomeOperation()
35+
{
36+
}
37+
```
38+
39+
## How to fix violations
40+
41+
To fix a violation of this rule, update the comment so the `<para>` element wraps the documentation text which follows
42+
it:
43+
44+
```csharp
45+
/// <summary>Summary text.</summary>
46+
/// <remarks>
47+
/// Remarks text.
48+
/// <para>Second paragraph of remarks, which is now placed inside the &lt;para&gt; element.</para>
49+
/// </remarks>
50+
public void SomeOperation()
51+
{
52+
}
53+
```
54+
55+
## How to suppress violations
56+
57+
Do not suppress violations of this rule. If the preferred documentation style does not align with the rule decription,
58+
it is best to disable the rule.

docs/StyleRules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Identifier | Name | Description
77
[DOC100](DOC100.md) | PlaceTextInParagraphs | A `<remarks>` or `<note>` documentation element contains content which is not wrapped in a block-level element.
88
[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.
99
[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.
10+
[DOC108](DOC108.md) | AvoidEmptyParagraphs | The documentation contains an empty paragraph element (`<para/>` or `<p/>`) used as a paragraph separator.

0 commit comments

Comments
 (0)