Skip to content

Commit d4ca1c6

Browse files
authored
Merge pull request #55 from sharwell/empty-para
Implement DOC108 (Avoid Empty Paragraphs)
2 parents 3d145af + 67dac52 commit d4ca1c6

File tree

7 files changed

+396
-0
lines changed

7 files changed

+396
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.Diagnostics;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using DocumentationAnalyzers.Helpers;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.CodeActions;
14+
using Microsoft.CodeAnalysis.CodeFixes;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
17+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DOC108CodeFixProvider))]
18+
[Shared]
19+
internal class DOC108CodeFixProvider : CodeFixProvider
20+
{
21+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
22+
= ImmutableArray.Create(DOC108AvoidEmptyParagraphs.DiagnosticId);
23+
24+
public override FixAllProvider GetFixAllProvider()
25+
=> CustomFixAllProviders.BatchFixer;
26+
27+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
28+
{
29+
foreach (var diagnostic in context.Diagnostics)
30+
{
31+
Debug.Assert(FixableDiagnosticIds.Contains(diagnostic.Id), "Assertion failed: FixableDiagnosticIds.Contains(diagnostic.Id)");
32+
33+
context.RegisterCodeFix(
34+
CodeAction.Create(
35+
StyleResources.DOC108CodeFix,
36+
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
37+
nameof(DOC108CodeFixProvider)),
38+
diagnostic);
39+
}
40+
41+
return SpecializedTasks.CompletedTask;
42+
}
43+
44+
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
45+
{
46+
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
47+
var xmlEmptyElement = (XmlEmptyElementSyntax)root.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true);
48+
return document.WithSyntaxRoot(root.RemoveNode(xmlEmptyElement, SyntaxRemoveOptions.KeepExteriorTrivia));
49+
}
50+
}
51+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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 TestEmptySeeElementSeparatesParagraphsAsync()
45+
{
46+
var testCode = @"
47+
/// <summary>
48+
/// Summary 1
49+
/// <see cref=""TestClass""/>
50+
/// Summary 2
51+
/// </summary>
52+
class TestClass
53+
{
54+
}
55+
";
56+
57+
await Verify.VerifyAnalyzerAsync(testCode);
58+
}
59+
60+
[Fact]
61+
public async Task TestPrefixedParagraphElementSeparatesParagraphsAsync()
62+
{
63+
var testCode = @"
64+
/// <summary>
65+
/// Summary 1
66+
/// <html:p/>
67+
/// Summary 2
68+
/// </summary>
69+
class TestClass
70+
{
71+
}
72+
";
73+
74+
await Verify.VerifyAnalyzerAsync(testCode);
75+
}
76+
77+
[Fact]
78+
public async Task TestEmptyElementWithMissingNameAsync()
79+
{
80+
var testCode = @"
81+
/// <summary>
82+
/// <
83+
/// </summary>
84+
class TestClass
85+
{
86+
}
87+
";
88+
89+
await Verify.VerifyAnalyzerAsync(testCode);
90+
}
91+
92+
[Fact]
93+
public async Task TestEmptyHtmlParagraphElementSeparatesParagraphsAsync()
94+
{
95+
var testCode = @"
96+
/// <summary>
97+
/// Summary 1
98+
/// $$<p/>
99+
/// Summary 2
100+
/// </summary>
101+
class TestClass
102+
{
103+
}
104+
";
105+
var fixedCode = @"
106+
/// <summary>
107+
/// Summary 1
108+
///
109+
/// Summary 2
110+
/// </summary>
111+
class TestClass
112+
{
113+
}
114+
";
115+
116+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
117+
}
118+
119+
[Fact]
120+
public async Task TestEmptyParagraphBeforeFullParagraphAsync()
121+
{
122+
var testCode = @"
123+
/// <summary>
124+
/// Summary 1
125+
/// $$<para/>
126+
/// <para>Summary 2</para>
127+
/// </summary>
128+
class TestClass
129+
{
130+
}
131+
";
132+
var fixedCode = @"
133+
/// <summary>
134+
/// Summary 1
135+
///
136+
/// <para>Summary 2</para>
137+
/// </summary>
138+
class TestClass
139+
{
140+
}
141+
";
142+
143+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
144+
}
145+
146+
[Fact]
147+
public async Task TestEmptyHtmlParagraphBeforeFullHtmlParagraphAsync()
148+
{
149+
var testCode = @"
150+
/// <summary>
151+
/// Summary 1
152+
/// $$<p/>
153+
/// <p>Summary 2</p>
154+
/// </summary>
155+
class TestClass
156+
{
157+
}
158+
";
159+
var fixedCode = @"
160+
/// <summary>
161+
/// Summary 1
162+
///
163+
/// <p>Summary 2</p>
164+
/// </summary>
165+
class TestClass
166+
{
167+
}
168+
";
169+
170+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
171+
}
172+
}
173+
}
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.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>

0 commit comments

Comments
 (0)