Skip to content

Commit 7fdbcef

Browse files
committed
Add PlaceholderDocumentation analyzer and code fix
This analyzer reports a warning for the use of <placeholder> elements in a documentation comment. If the element contains text, a code fix is also provided which allows the contents to be "finalized" by simply removing the start and end placeholder tags. Fixes #10
1 parent cf6a0a1 commit 7fdbcef

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
<Compile Include="JsonPropertyDefaultValueHandlingAnalyzer.cs" />
4343
<Compile Include="JsonPropertyDefaultValueHandlingCodeFix.cs" />
4444
<Compile Include="NonNullableJsonPropertyAnalyzer.cs" />
45+
<Compile Include="PlaceholderDocumentationAnalyzer.cs" />
46+
<Compile Include="PlaceholderDocumentationCodeFix.cs" />
4547
<Compile Include="Properties\AssemblyInfo.cs" />
4648
<Compile Include="SpacingExtensions.cs" />
4749
<Compile Include="XmlSyntaxFactory.cs" />
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class PlaceholderDocumentationAnalyzer : DiagnosticAnalyzer
12+
{
13+
public const string DiagnosticId = "PlaceholderDocumentation";
14+
internal const string Title = "Do not use placeholders in documentation";
15+
internal const string MessageFormat = "Do not use placeholders in documentation";
16+
internal const string Category = "OpenStack.Documentation";
17+
internal const string Description = "Do not use placeholders in documentation";
18+
19+
private static DiagnosticDescriptor Descriptor =
20+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
21+
22+
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
23+
ImmutableArray.Create(Descriptor);
24+
25+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
26+
{
27+
get
28+
{
29+
return _supportedDiagnostics;
30+
}
31+
}
32+
33+
public override void Initialize(AnalysisContext context)
34+
{
35+
context.RegisterSyntaxNodeAction(HandleXmlElement, SyntaxKind.XmlElement);
36+
context.RegisterSyntaxNodeAction(HandleXmlEmptyElement, SyntaxKind.XmlEmptyElement);
37+
}
38+
39+
private void HandleXmlElement(SyntaxNodeAnalysisContext context)
40+
{
41+
XmlElementSyntax syntax = (XmlElementSyntax)context.Node;
42+
if (!string.Equals("placeholder", syntax.StartTag?.Name?.ToString(), StringComparison.Ordinal))
43+
return;
44+
45+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, syntax.GetLocation()));
46+
}
47+
48+
private void HandleXmlEmptyElement(SyntaxNodeAnalysisContext context)
49+
{
50+
XmlEmptyElementSyntax syntax = (XmlEmptyElementSyntax)context.Node;
51+
if (!string.Equals("placeholder", syntax.Name?.ToString(), StringComparison.Ordinal))
52+
return;
53+
54+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, syntax.GetLocation()));
55+
}
56+
}
57+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CodeActions;
10+
using Microsoft.CodeAnalysis.CodeFixes;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
13+
[ExportCodeFixProvider(nameof(PlaceholderDocumentationCodeFix), LanguageNames.CSharp)]
14+
[Shared]
15+
public class PlaceholderDocumentationCodeFix : CodeFixProvider
16+
{
17+
private static readonly ImmutableArray<string> _fixableDiagnostics =
18+
ImmutableArray.Create(PlaceholderDocumentationAnalyzer.DiagnosticId);
19+
20+
public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
21+
{
22+
return _fixableDiagnostics;
23+
}
24+
25+
public override FixAllProvider GetFixAllProvider()
26+
{
27+
// Require users review each removal of placeholder tags.
28+
return null;
29+
}
30+
31+
public override async Task ComputeFixesAsync(CodeFixContext context)
32+
{
33+
foreach (var diagnostic in context.Diagnostics)
34+
{
35+
if (!string.Equals(diagnostic.Id, PlaceholderDocumentationAnalyzer.DiagnosticId, StringComparison.Ordinal))
36+
continue;
37+
38+
var documentRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
39+
SyntaxNode syntax = documentRoot.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true);
40+
if (syntax == null)
41+
continue;
42+
43+
XmlElementSyntax xmlElementSyntax = syntax as XmlElementSyntax;
44+
if (xmlElementSyntax == null)
45+
{
46+
// We continue even for placeholders if they are empty elements (XmlEmptyElementSyntax)
47+
continue;
48+
}
49+
50+
if (string.IsNullOrWhiteSpace(xmlElementSyntax.Content.ToString()))
51+
{
52+
// The placeholder hasn't been updated yet.
53+
continue;
54+
}
55+
56+
string description = "Finalize placeholder text";
57+
context.RegisterFix(CodeAction.Create(description, cancellationToken => CreateChangedDocument(context, xmlElementSyntax, cancellationToken)), diagnostic);
58+
}
59+
}
60+
61+
private async Task<Document> CreateChangedDocument(CodeFixContext context, XmlElementSyntax elementSyntax, CancellationToken cancellationToken)
62+
{
63+
SyntaxList<XmlNodeSyntax> content = elementSyntax.Content;
64+
if (content.Count == 0)
65+
return context.Document;
66+
67+
var leadingTrivia = elementSyntax.StartTag.GetLeadingTrivia();
68+
leadingTrivia = leadingTrivia.AddRange(elementSyntax.EndTag.GetTrailingTrivia());
69+
leadingTrivia = leadingTrivia.AddRange(content[0].GetLeadingTrivia());
70+
content = content.Replace(content[0], content[0].WithLeadingTrivia(leadingTrivia));
71+
72+
var trailingTrivia = content[content.Count - 1].GetTrailingTrivia();
73+
trailingTrivia = trailingTrivia.AddRange(elementSyntax.EndTag.GetLeadingTrivia());
74+
trailingTrivia = trailingTrivia.AddRange(elementSyntax.EndTag.GetTrailingTrivia());
75+
content = content.Replace(content[content.Count - 1], content[content.Count - 1].WithTrailingTrivia(trailingTrivia));
76+
77+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken);
78+
SyntaxNode newRoot = root.ReplaceNode(elementSyntax, content);
79+
return context.Document.WithSyntaxRoot(newRoot);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)