Skip to content

Commit c0c4771

Browse files
authored
Merge pull request #18 from delegateas/CT0006
Add CT0006 analyzer to enforce XML documentation on Plugin subclasses
2 parents 56f7fa2 + c18d749 commit c0c4771

File tree

4 files changed

+491
-0
lines changed

4 files changed

+491
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace DataverseAnalyzer;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class PluginDocumentationAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly Lazy<DiagnosticDescriptor> LazyRule = new(() => new DiagnosticDescriptor(
13+
"CT0006",
14+
Resources.CT0006_Title,
15+
Resources.CT0006_MessageFormat,
16+
"Documentation",
17+
DiagnosticSeverity.Warning,
18+
isEnabledByDefault: true,
19+
description: Resources.CT0006_Description));
20+
21+
public static DiagnosticDescriptor Rule => LazyRule.Value;
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
24+
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
if (context is null)
28+
{
29+
throw new ArgumentNullException(nameof(context));
30+
}
31+
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
33+
context.EnableConcurrentExecution();
34+
35+
context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
36+
}
37+
38+
private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
39+
{
40+
var classDeclaration = (ClassDeclarationSyntax)context.Node;
41+
42+
if (!InheritsFromPlugin(classDeclaration))
43+
return;
44+
45+
if (HasValidDocumentation(classDeclaration))
46+
return;
47+
48+
var diagnostic = Diagnostic.Create(
49+
Rule,
50+
classDeclaration.Identifier.GetLocation(),
51+
classDeclaration.Identifier.ValueText);
52+
53+
context.ReportDiagnostic(diagnostic);
54+
}
55+
56+
private static bool InheritsFromPlugin(ClassDeclarationSyntax classDeclaration)
57+
{
58+
if (classDeclaration.BaseList is null)
59+
return false;
60+
61+
foreach (var baseType in classDeclaration.BaseList.Types)
62+
{
63+
var typeName = GetBaseTypeName(baseType.Type);
64+
if (typeName == "Plugin")
65+
return true;
66+
}
67+
68+
return false;
69+
}
70+
71+
private static string? GetBaseTypeName(TypeSyntax type)
72+
{
73+
return type switch
74+
{
75+
IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
76+
QualifiedNameSyntax qualified => qualified.Right.Identifier.ValueText,
77+
_ => null,
78+
};
79+
}
80+
81+
private static bool HasValidDocumentation(ClassDeclarationSyntax classDeclaration)
82+
{
83+
var leadingTrivia = classDeclaration.GetLeadingTrivia();
84+
85+
foreach (var trivia in leadingTrivia)
86+
{
87+
if (!trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
88+
continue;
89+
90+
var structure = trivia.GetStructure();
91+
if (structure is null)
92+
continue;
93+
94+
if (HasInheritdoc(structure))
95+
return true;
96+
97+
if (HasNonEmptySummary(structure))
98+
return true;
99+
}
100+
101+
return false;
102+
}
103+
104+
private static bool HasInheritdoc(SyntaxNode structure)
105+
{
106+
foreach (var node in structure.DescendantNodes())
107+
{
108+
if (node is XmlEmptyElementSyntax emptyElement &&
109+
emptyElement.Name.LocalName.ValueText == "inheritdoc")
110+
{
111+
return true;
112+
}
113+
}
114+
115+
return false;
116+
}
117+
118+
private static bool HasNonEmptySummary(SyntaxNode structure)
119+
{
120+
foreach (var node in structure.DescendantNodes())
121+
{
122+
if (node is not XmlElementSyntax element)
123+
continue;
124+
125+
if (element.StartTag.Name.LocalName.ValueText != "summary")
126+
continue;
127+
128+
var content = element.Content;
129+
foreach (var contentNode in content)
130+
{
131+
if (contentNode is XmlTextSyntax textSyntax)
132+
{
133+
var text = string.Concat(textSyntax.TextTokens.Select(t => t.ValueText));
134+
if (!string.IsNullOrWhiteSpace(text))
135+
return true;
136+
}
137+
}
138+
}
139+
140+
return false;
141+
}
142+
}

src/DataverseAnalyzer/Resources.Designer.cs

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

src/DataverseAnalyzer/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,13 @@
109109
<data name="CT0005_Description" xml:space="preserve">
110110
<value>Having multiple constructor parameters of the same dependency injection type (Service, Repository, Handler, Provider, Factory, Manager, Client) can lead to confusion and accidental parameter swapping.</value>
111111
</data>
112+
<data name="CT0006_Title" xml:space="preserve">
113+
<value>Plugin class missing XML documentation summary</value>
114+
</data>
115+
<data name="CT0006_MessageFormat" xml:space="preserve">
116+
<value>Plugin class '{0}' should have a short XML documentation summary describing its purpose</value>
117+
</data>
118+
<data name="CT0006_Description" xml:space="preserve">
119+
<value>Classes that inherit from Plugin should have a short XML summary comment explaining what the plugin does.</value>
120+
</data>
112121
</root>

0 commit comments

Comments
 (0)