Skip to content

Commit cf6a0a1

Browse files
committed
Add DocumentDelegatingApiCall analyzer and code fix
This analyzer adds initial documentation to HTTP API call classes. Information in the summary element which needs to be verified is wrapped in placeholder elements. Fixes #1
1 parent ec77759 commit cf6a0a1

4 files changed

Lines changed: 415 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class DocumentDelegatingApiCallAnalyzer : DiagnosticAnalyzer
11+
{
12+
public const string DiagnosticId = "DocumentDelegatingApiCall";
13+
internal const string Title = "Document delegating HTTP API call";
14+
internal const string MessageFormat = "Document delegating HTTP API call";
15+
internal const string Category = "OpenStack.Documentation";
16+
internal const string Description = "Document delegating HTTP API call";
17+
18+
private static DiagnosticDescriptor Descriptor =
19+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
20+
21+
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
22+
ImmutableArray.Create(Descriptor);
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
25+
{
26+
get
27+
{
28+
return _supportedDiagnostics;
29+
}
30+
}
31+
32+
public override void Initialize(AnalysisContext context)
33+
{
34+
context.RegisterSymbolAction(HandleNamedType, SymbolKind.NamedType);
35+
}
36+
37+
private void HandleNamedType(SymbolAnalysisContext context)
38+
{
39+
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
40+
if (symbol.TypeKind != TypeKind.Class)
41+
return;
42+
43+
if (!IsDelegatingHttpApiCall(context, symbol))
44+
return;
45+
46+
if (!string.IsNullOrEmpty(symbol.GetDocumentationCommentXml(cancellationToken: context.CancellationToken)))
47+
return;
48+
49+
var locations = symbol.Locations;
50+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, locations.FirstOrDefault(), locations.Skip(1)));
51+
}
52+
53+
private bool IsDelegatingHttpApiCall(SymbolAnalysisContext context, INamedTypeSymbol symbol)
54+
{
55+
while (symbol != null && symbol.SpecialType != SpecialType.System_Object)
56+
{
57+
if (symbol.IsGenericType)
58+
{
59+
var originalDefinition = symbol.OriginalDefinition;
60+
string fullyQualifiedName = originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
61+
if (string.Equals("global::OpenStack.Net.DelegatingHttpApiCall<T>", fullyQualifiedName, StringComparison.Ordinal))
62+
return true;
63+
}
64+
65+
symbol = symbol.BaseType;
66+
}
67+
68+
return false;
69+
}
70+
}
71+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Composition;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.CodeFixes;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
using Microsoft.CodeAnalysis.Simplification;
15+
16+
[ExportCodeFixProvider(nameof(DocumentDelegatingApiCallCodeFix), LanguageNames.CSharp)]
17+
[Shared]
18+
public class DocumentDelegatingApiCallCodeFix : CodeFixProvider
19+
{
20+
private static readonly ImmutableArray<string> _fixableDiagnostics =
21+
ImmutableArray.Create(DocumentDelegatingApiCallAnalyzer.DiagnosticId);
22+
23+
public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
24+
{
25+
return _fixableDiagnostics;
26+
}
27+
28+
public override FixAllProvider GetFixAllProvider()
29+
{
30+
return WellKnownFixAllProviders.BatchFixer;
31+
}
32+
33+
public override async Task ComputeFixesAsync(CodeFixContext context)
34+
{
35+
foreach (var diagnostic in context.Diagnostics)
36+
{
37+
if (!string.Equals(diagnostic.Id, DocumentDelegatingApiCallAnalyzer.DiagnosticId, StringComparison.Ordinal))
38+
continue;
39+
40+
var documentRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
41+
SyntaxNode syntax = documentRoot.FindNode(diagnostic.Location.SourceSpan);
42+
if (syntax == null)
43+
continue;
44+
45+
ClassDeclarationSyntax classDeclarationSyntax = syntax.FirstAncestorOrSelf<ClassDeclarationSyntax>();
46+
if (classDeclarationSyntax == null)
47+
continue;
48+
49+
string description = "Add documentation for delegating HTTP API call";
50+
context.RegisterFix(CodeAction.Create(description, cancellationToken => CreateChangedDocument(context, classDeclarationSyntax, cancellationToken)), diagnostic);
51+
}
52+
}
53+
54+
private async Task<Document> CreateChangedDocument(CodeFixContext context, ClassDeclarationSyntax classDeclarationSyntax, CancellationToken cancellationToken)
55+
{
56+
string serviceInterfaceName = "IUnknownService";
57+
string serviceExtensionsClassName = "UnknownServiceExtensions";
58+
INamedTypeSymbol serviceInterface = await GetServiceInterfaceAsync(context, classDeclarationSyntax, cancellationToken);
59+
if (serviceInterface != null)
60+
{
61+
serviceInterfaceName = serviceInterface.MetadataName;
62+
serviceExtensionsClassName = serviceInterfaceName + "Extensions";
63+
if (serviceInterfaceName.StartsWith("I"))
64+
serviceExtensionsClassName = serviceExtensionsClassName.Substring(1);
65+
}
66+
67+
string fullServiceName = "Unknown Service";
68+
if (serviceInterface != null)
69+
fullServiceName = ExtractFullServiceName(serviceInterface);
70+
71+
string callName = classDeclarationSyntax.Identifier.ValueText;
72+
int apiCallSuffix = callName.IndexOf("ApiCall", StringComparison.Ordinal);
73+
if (apiCallSuffix > 0)
74+
callName = callName.Substring(0, apiCallSuffix);
75+
76+
ClassDeclarationSyntax newClassDeclaration = classDeclarationSyntax;
77+
78+
ConstructorDeclarationSyntax constructor = await FindApiCallConstructorAsync(context, classDeclarationSyntax, cancellationToken);
79+
ConstructorDeclarationSyntax newConstructor = await DocumentConstructorAsync(context, constructor, cancellationToken);
80+
if (newConstructor != null)
81+
newClassDeclaration = newClassDeclaration.ReplaceNode(constructor, newConstructor);
82+
83+
DocumentationCommentTriviaSyntax documentationComment = XmlSyntaxFactory.DocumentationComment(
84+
XmlSyntaxFactory.SummaryElement(
85+
XmlSyntaxFactory.Text("This class represents an HTTP API call to "),
86+
XmlSyntaxFactory.PlaceholderElement(XmlSyntaxFactory.Text(callName)),
87+
XmlSyntaxFactory.Text(" with the "),
88+
XmlSyntaxFactory.PlaceholderElement(XmlSyntaxFactory.Text(fullServiceName))),
89+
XmlSyntaxFactory.NewLine(),
90+
XmlSyntaxFactory.SeeAlsoElement(SyntaxFactory.NameMemberCref(SyntaxFactory.ParseName($"{serviceInterfaceName}.Prepare{callName}Async"))),
91+
XmlSyntaxFactory.NewLine(),
92+
XmlSyntaxFactory.SeeAlsoElement(SyntaxFactory.NameMemberCref(SyntaxFactory.ParseName($"{serviceExtensionsClassName}.{callName}Async"))),
93+
XmlSyntaxFactory.NewLine(),
94+
XmlSyntaxFactory.ThreadSafetyElement(),
95+
XmlSyntaxFactory.NewLine(),
96+
XmlSyntaxFactory.PreliminaryElement())
97+
.WithAdditionalAnnotations(Simplifier.Annotation);
98+
99+
SyntaxTrivia documentationTrivia = SyntaxFactory.Trivia(documentationComment);
100+
newClassDeclaration = newClassDeclaration.WithLeadingTrivia(newClassDeclaration.GetLeadingTrivia().Add(documentationTrivia));
101+
102+
SyntaxNode root = await context.Document.GetSyntaxRootAsync(cancellationToken);
103+
SyntaxNode newRoot = root.ReplaceNode(classDeclarationSyntax, newClassDeclaration);
104+
return context.Document.WithSyntaxRoot(newRoot);
105+
}
106+
107+
private async Task<ConstructorDeclarationSyntax> DocumentConstructorAsync(CodeFixContext context, ConstructorDeclarationSyntax constructor, CancellationToken cancellationToken)
108+
{
109+
if (constructor == null)
110+
return null;
111+
112+
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken);
113+
INamedTypeSymbol apiCallClass = semanticModel.GetDeclaredSymbol(constructor.FirstAncestorOrSelf<ClassDeclarationSyntax>(), cancellationToken);
114+
string parameterName = constructor.ParameterList.Parameters[0].Identifier.ValueText;
115+
116+
DocumentationCommentTriviaSyntax documentationComment = XmlSyntaxFactory.DocumentationComment(
117+
XmlSyntaxFactory.SummaryElement(
118+
XmlSyntaxFactory.Text("Initializes a new instance of the "),
119+
XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(apiCallClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))),
120+
XmlSyntaxFactory.Text(" class"),
121+
XmlSyntaxFactory.NewLine(),
122+
XmlSyntaxFactory.Text("with the behavior provided by another "),
123+
XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName("global::OpenStack.Net.IHttpApiCall{T}"))),
124+
XmlSyntaxFactory.Text(" instance.")),
125+
XmlSyntaxFactory.NewLine(),
126+
XmlSyntaxFactory.ParamElement(
127+
parameterName,
128+
XmlSyntaxFactory.List(
129+
XmlSyntaxFactory.Text("The "),
130+
XmlSyntaxFactory.SeeElement(SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName("global::OpenStack.Net.IHttpApiCall{T}"))),
131+
XmlSyntaxFactory.Text(" providing the behavior for the API call."))),
132+
XmlSyntaxFactory.NewLine(),
133+
XmlSyntaxFactory.ExceptionElement(
134+
SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName("global::System.ArgumentNullException")),
135+
XmlSyntaxFactory.List(
136+
XmlSyntaxFactory.Text("If "),
137+
XmlSyntaxFactory.ParamRefElement(parameterName),
138+
XmlSyntaxFactory.Text(" is "),
139+
XmlSyntaxFactory.NullKeywordElement(),
140+
XmlSyntaxFactory.Text("."))))
141+
.WithAdditionalAnnotations(Simplifier.Annotation);
142+
143+
SyntaxTrivia documentationTrivia = SyntaxFactory.Trivia(documentationComment);
144+
return constructor.WithLeadingTrivia(constructor.GetLeadingTrivia().Add(documentationTrivia));
145+
}
146+
147+
private async Task<ConstructorDeclarationSyntax> FindApiCallConstructorAsync(CodeFixContext context, ClassDeclarationSyntax classDeclarationSyntax, CancellationToken cancellationToken)
148+
{
149+
SemanticModel semanticModel = null;
150+
151+
foreach (var constructorSyntax in classDeclarationSyntax.Members.OfType<ConstructorDeclarationSyntax>())
152+
{
153+
// We are looking for a constructor with exactly one parameter. The '==' operator here is lifted-to-null,
154+
// allowing a single condition to cover both cases where a syntax element is missing (null) and
155+
// constructors with the wrong number of arguments.
156+
if (!(constructorSyntax.ParameterList?.Parameters.Count == 1))
157+
continue;
158+
159+
ParameterSyntax firstParameter = constructorSyntax.ParameterList.Parameters[0];
160+
if (firstParameter.Identifier.IsMissing)
161+
continue;
162+
163+
TypeSyntax parameterType = firstParameter.Type;
164+
if (parameterType == null)
165+
continue;
166+
167+
if (semanticModel == null)
168+
semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken);
169+
170+
INamedTypeSymbol symbol = semanticModel.GetSymbolInfo(parameterType, cancellationToken).Symbol as INamedTypeSymbol;
171+
if (symbol == null || !symbol.IsGenericType)
172+
continue;
173+
174+
string fullName = symbol.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
175+
string expectedName = "global::OpenStack.Net.IHttpApiCall<T>";
176+
if (string.Equals(expectedName, fullName, StringComparison.Ordinal))
177+
return constructorSyntax;
178+
}
179+
180+
return null;
181+
}
182+
183+
private string ExtractFullServiceName(INamedTypeSymbol serviceInterface)
184+
{
185+
string vendor = ExtractVendorName(serviceInterface);
186+
string version = ExtractServiceVersion(serviceInterface);
187+
string service = ExtractServiceName(serviceInterface);
188+
return $"{vendor} {service} Service {version}";
189+
}
190+
191+
private string ExtractVendorName(INamedTypeSymbol serviceInterface)
192+
{
193+
INamespaceSymbol topLevelNamespace = serviceInterface.ContainingNamespace;
194+
while (topLevelNamespace != null)
195+
{
196+
if (topLevelNamespace.ContainingNamespace == null || topLevelNamespace.ContainingNamespace.IsGlobalNamespace)
197+
break;
198+
199+
topLevelNamespace = topLevelNamespace.ContainingNamespace;
200+
}
201+
202+
if (topLevelNamespace == null || topLevelNamespace.IsGlobalNamespace)
203+
return "[Vendor]";
204+
205+
return topLevelNamespace.Name;
206+
}
207+
208+
private string ExtractServiceVersion(INamedTypeSymbol serviceInterface)
209+
{
210+
const string UnknownVersion = "[Version]";
211+
if (serviceInterface.ContainingNamespace == null)
212+
return UnknownVersion;
213+
214+
if (serviceInterface.ContainingNamespace.IsGlobalNamespace)
215+
return UnknownVersion;
216+
217+
string version = serviceInterface.ContainingNamespace.Name;
218+
if (!version.StartsWith("V"))
219+
return UnknownVersion;
220+
221+
return version;
222+
}
223+
224+
private string ExtractServiceName(INamedTypeSymbol serviceInterface)
225+
{
226+
string interfaceName = serviceInterface.Name;
227+
if (interfaceName.StartsWith("I"))
228+
interfaceName = interfaceName.Substring(1);
229+
230+
if (interfaceName.EndsWith("Service"))
231+
interfaceName = interfaceName.Substring(0, interfaceName.LastIndexOf("Service"));
232+
233+
return interfaceName;
234+
}
235+
236+
private async Task<INamedTypeSymbol> GetServiceInterfaceAsync(CodeFixContext context, ClassDeclarationSyntax classDeclarationSyntax, CancellationToken cancellationToken)
237+
{
238+
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken);
239+
INamedTypeSymbol apiCallSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken);
240+
foreach (INamedTypeSymbol type in apiCallSymbol.ContainingNamespace.GetTypeMembers())
241+
{
242+
if (type.TypeKind != TypeKind.Interface)
243+
continue;
244+
245+
foreach (INamedTypeSymbol interfaceType in type.AllInterfaces)
246+
{
247+
if (string.Equals("IHttpService", interfaceType.MetadataName))
248+
return type;
249+
}
250+
}
251+
252+
return null;
253+
}
254+
255+
private bool IsServiceInterface(SemanticModel semanticModel, INamedTypeSymbol symbol)
256+
{
257+
while (symbol != null && symbol.SpecialType != SpecialType.System_Object)
258+
{
259+
if (symbol.IsGenericType)
260+
{
261+
var originalDefinition = symbol.OriginalDefinition;
262+
string fullyQualifiedName = originalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
263+
if (string.Equals("global::OpenStack.Net.DelegatingHttpApiCall<T>", fullyQualifiedName, StringComparison.Ordinal))
264+
return true;
265+
}
266+
267+
symbol = symbol.BaseType;
268+
}
269+
270+
return false;
271+
}
272+
}
273+
}

OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
</PropertyGroup>
3434
<ItemGroup>
3535
<Compile Include="AssertNullAnalyzer.cs" />
36+
<Compile Include="DocumentDelegatingApiCallAnalyzer.cs" />
37+
<Compile Include="DocumentDelegatingApiCallCodeFix.cs" />
3638
<Compile Include="ImplementBuilderPatternAnalyzer.cs" />
3739
<Compile Include="ImplementBuilderPatternCodeFix.cs" />
3840
<Compile Include="JsonObjectOptInAnalyzer.cs" />

0 commit comments

Comments
 (0)