Skip to content

Commit 74ed524

Browse files
committed
Implement DOC201 (Item Should Have Description)
Closes #26
1 parent 58d2cf5 commit 74ed524

8 files changed

Lines changed: 350 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.PortabilityRules
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(DOC201CodeFixProvider))]
17+
[Shared]
18+
internal class DOC201CodeFixProvider : CodeFixProvider
19+
{
20+
public override ImmutableArray<string> FixableDiagnosticIds { get; }
21+
= ImmutableArray.Create(DOC201ItemShouldHaveDescription.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+
PortabilityResources.DOC201CodeFix,
38+
token => GetTransformedDocumentAsync(context.Document, diagnostic, token),
39+
nameof(DOC201CodeFixProvider)),
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+
SyntaxToken token = root.FindToken(diagnostic.Location.SourceSpan.Start, findInsideTrivia: true);
50+
51+
var xmlElement = token.Parent.FirstAncestorOrSelf<XmlElementSyntax>();
52+
var newXmlElement = xmlElement.WithContent(XmlSyntaxFactory.List(XmlSyntaxFactory.Element(XmlCommentHelper.DescriptionXmlTag, xmlElement.Content)));
53+
return document.WithSyntaxRoot(root.ReplaceNode(xmlElement, newXmlElement));
54+
}
55+
}
56+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.CSharp7.PortabilityRules
5+
{
6+
using DocumentationAnalyzers.Test.PortabilityRules;
7+
8+
public class DOC201CSharp7UnitTests : DOC201UnitTests
9+
{
10+
}
11+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.PortabilityRules
5+
{
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<DocumentationAnalyzers.PortabilityRules.DOC201ItemShouldHaveDescription, DocumentationAnalyzers.PortabilityRules.DOC201CodeFixProvider, Microsoft.CodeAnalysis.Testing.Verifiers.XUnitVerifier>;
9+
10+
public class DOC201UnitTests
11+
{
12+
[Fact]
13+
public async Task TestListItemsWithoutDescriptionAsync()
14+
{
15+
var testCode = @"
16+
/// <remarks>
17+
/// <list type=""number"">
18+
/// <[|item|]>Item 1</item>
19+
/// <[|item|]>Item 2</item>
20+
/// </list>
21+
/// </remarks>
22+
class TestClass { }
23+
";
24+
var fixedCode = @"
25+
/// <remarks>
26+
/// <list type=""number"">
27+
/// <item><description>Item 1</description></item>
28+
/// <item><description>Item 2</description></item>
29+
/// </list>
30+
/// </remarks>
31+
class TestClass { }
32+
";
33+
34+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
35+
}
36+
37+
[Fact]
38+
public async Task TestMultilineListItemsWithoutDescriptionAsync()
39+
{
40+
var testCode = @"
41+
/// <remarks>
42+
/// <list type=""number"">
43+
/// <[|item|]>
44+
/// Item 1
45+
/// </item>
46+
/// <[|item|]>
47+
/// Item 2
48+
/// </item>
49+
/// </list>
50+
/// </remarks>
51+
class TestClass { }
52+
";
53+
var fixedCode = @"
54+
/// <remarks>
55+
/// <list type=""number"">
56+
/// <item><description>
57+
/// Item 1
58+
/// </description></item>
59+
/// <item><description>
60+
/// Item 2
61+
/// </description></item>
62+
/// </list>
63+
/// </remarks>
64+
class TestClass { }
65+
";
66+
67+
await Verify.VerifyCodeFixAsync(testCode, fixedCode);
68+
}
69+
70+
[Fact]
71+
public async Task TestListItemsWithEmptyDescriptionAsync()
72+
{
73+
var testCode = @"
74+
/// <remarks>
75+
/// <list type=""number"">
76+
/// <item><description></description></item>
77+
/// <item><description/></item>
78+
/// </list>
79+
/// </remarks>
80+
class TestClass { }
81+
";
82+
83+
await Verify.VerifyAnalyzerAsync(testCode);
84+
}
85+
86+
[Fact]
87+
public async Task TestListItemsWithTermAsync()
88+
{
89+
var testCode = @"
90+
/// <remarks>
91+
/// <list type=""number"">
92+
/// <item><term>Item 1</term></item>
93+
/// <item><term>Item 2</term><description>Description</description></item>
94+
/// </list>
95+
/// </remarks>
96+
class TestClass { }
97+
";
98+
99+
await Verify.VerifyAnalyzerAsync(testCode);
100+
}
101+
}
102+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.PortabilityRules
5+
{
6+
using System.Collections.Immutable;
7+
using DocumentationAnalyzers.Helpers;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
13+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
14+
internal class DOC201ItemShouldHaveDescription : DiagnosticAnalyzer
15+
{
16+
/// <summary>
17+
/// The ID for diagnostics produced by the <see cref="DOC201ItemShouldHaveDescription"/> analyzer.
18+
/// </summary>
19+
public const string DiagnosticId = "DOC201";
20+
private const string HelpLink = "https://github.com/DotNetAnalyzers/DocumentationAnalyzers/blob/master/docs/DOC201.md";
21+
22+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(PortabilityResources.DOC201Title), PortabilityResources.ResourceManager, typeof(PortabilityResources));
23+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(PortabilityResources.DOC201MessageFormat), PortabilityResources.ResourceManager, typeof(PortabilityResources));
24+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(PortabilityResources.DOC201Description), PortabilityResources.ResourceManager, typeof(PortabilityResources));
25+
26+
private static readonly DiagnosticDescriptor Descriptor =
27+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.PortabilityRules, DiagnosticSeverity.Info, AnalyzerConstants.EnabledByDefault, Description, HelpLink);
28+
29+
/// <inheritdoc/>
30+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
31+
= ImmutableArray.Create(Descriptor);
32+
33+
public override void Initialize(AnalysisContext context)
34+
{
35+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
36+
context.EnableConcurrentExecution();
37+
38+
context.RegisterSyntaxNodeAction(HandleXmlElementSyntax, SyntaxKind.XmlElement);
39+
}
40+
41+
private static void HandleXmlElementSyntax(SyntaxNodeAnalysisContext context)
42+
{
43+
var xmlElementSyntax = (XmlElementSyntax)context.Node;
44+
var name = xmlElementSyntax.StartTag?.Name;
45+
if (name is null || name.Prefix != null)
46+
{
47+
return;
48+
}
49+
50+
switch (name.LocalName.ValueText)
51+
{
52+
case XmlCommentHelper.ItemXmlTag:
53+
break;
54+
55+
default:
56+
return;
57+
}
58+
59+
// check for a <term> or <description> child element
60+
foreach (var node in xmlElementSyntax.Content)
61+
{
62+
var childName = node.GetName();
63+
if (childName is null || childName.Prefix != null)
64+
{
65+
continue;
66+
}
67+
68+
switch (childName.LocalName.ValueText)
69+
{
70+
case XmlCommentHelper.TermXmlTag:
71+
case XmlCommentHelper.DescriptionXmlTag:
72+
// Avoid analyzing <item> that already has <term> and/or <description>
73+
return;
74+
75+
default:
76+
break;
77+
}
78+
}
79+
80+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, name.LocalName.GetLocation()));
81+
}
82+
}
83+
}

DocumentationAnalyzers/DocumentationAnalyzers/PortabilityRules/PortabilityResources.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/PortabilityRules/PortabilityResources.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,16 @@
129129
<data name="DOC200Title" xml:space="preserve">
130130
<value>Use XML documentation syntax</value>
131131
</data>
132+
<data name="DOC201CodeFix" xml:space="preserve">
133+
<value>Item should have description</value>
134+
</data>
135+
<data name="DOC201Description" xml:space="preserve">
136+
<value>Item should have description</value>
137+
</data>
138+
<data name="DOC201MessageFormat" xml:space="preserve">
139+
<value>Item should have description</value>
140+
</data>
141+
<data name="DOC201Title" xml:space="preserve">
142+
<value>Item should have description</value>
143+
</data>
132144
</root>

docs/DOC201.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# DOC201
2+
3+
<table>
4+
<tr>
5+
<td>TypeName</td>
6+
<td>DOC201ItemShouldHaveDescription</td>
7+
</tr>
8+
<tr>
9+
<td>CheckId</td>
10+
<td>DOC201</td>
11+
</tr>
12+
<tr>
13+
<td>Category</td>
14+
<td>Portability Rules</td>
15+
</tr>
16+
</table>
17+
18+
## Cause
19+
20+
The documentation for an `<item>` within a `<list>` did not include the required `<term>` and/or `<description>`
21+
elements.
22+
23+
## Rule description
24+
25+
According to the C# language specification, the `<item>` element within a documentation comment must have its content
26+
wrapped in a `<description>` element. Not all documentation processing tools support omitting the `<description>`
27+
element, so it should be included for consistent behavior.
28+
29+
See [dotnet/csharplang#1765](https://github.com/dotnet/csharplang/issues/1765) for a language proposal to natively
30+
support lists with the `<description>` element removed.
31+
32+
## How to fix violations
33+
34+
To fix a violation of this rule, wrap the content of the `<item>` element in a `<description>` element.
35+
36+
## How to suppress violations
37+
38+
```csharp
39+
#pragma warning disable DOC201 // Item should have description
40+
/// <remarks>
41+
/// <list type="bullet">
42+
/// <item>This item has text not wrapped in a description element.</item>
43+
/// </list>
44+
/// </remarks>
45+
public void SomeOperation()
46+
#pragma warning restore DOC201 // Item should have description
47+
{
48+
}
49+
```

docs/PortabilityRules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Rules related to the portability of documentation comments.
55
Identifier | Name | Description
66
-----------|------|-------------
77
[DOC200](DOC200.md) | UseXmlDocumentationSyntax | The documentation for the element an HTML element equivalent to a known XML documentation element.
8+
[DOC201](DOC201.md) | ItemShouldHaveDescription | The documentation for an `<item>` within a `<list>` did not include the required `<term>` and/or `<description>` elements.

0 commit comments

Comments
 (0)