Skip to content

Commit 2540b39

Browse files
committed
Merge pull request #21 from sharwell/fix-17
DocumentNullJsonValue analyzer
2 parents 92f45aa + 2877ec7 commit 2540b39

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
namespace OpenStackNetAnalyzers
2+
{
3+
using System;
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public class DocumentNullJsonValueAnalyzer : DiagnosticAnalyzer
13+
{
14+
public const string DiagnosticId = "DocumentNullJsonValue";
15+
internal const string Title = "Document null JSON value";
16+
internal const string MessageFormat = "The <value> documentation for JSON property with type '{0}' should include '<token>{1}</token>'";
17+
internal const string Category = "OpenStack.Documentation";
18+
internal const string Description = "The <value> documentation for JSON properties should include NullIfNotIncluded or DefaultArrayIfNotIncluded";
19+
20+
private static DiagnosticDescriptor Descriptor =
21+
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
22+
23+
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics =
24+
ImmutableArray.Create(Descriptor);
25+
26+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
27+
{
28+
get
29+
{
30+
return _supportedDiagnostics;
31+
}
32+
}
33+
34+
public override void Initialize(AnalysisContext context)
35+
{
36+
context.RegisterSymbolAction(HandleNamedType, SymbolKind.NamedType);
37+
}
38+
39+
private void HandleNamedType(SymbolAnalysisContext context)
40+
{
41+
INamedTypeSymbol symbol = (INamedTypeSymbol)context.Symbol;
42+
if (symbol.TypeKind != TypeKind.Class)
43+
return;
44+
45+
if (!symbol.IsExtensibleJsonObject())
46+
return;
47+
48+
foreach (var propertySymbol in symbol.GetMembers().OfType<IPropertySymbol>())
49+
{
50+
if (propertySymbol.SetMethod != null)
51+
continue;
52+
53+
var locations = propertySymbol.Locations;
54+
if (locations.IsDefaultOrEmpty)
55+
continue;
56+
57+
var tree = locations[0].SourceTree;
58+
if (tree == null)
59+
continue;
60+
61+
var root = tree.GetRoot(context.CancellationToken);
62+
var node = root.FindNode(locations[0].SourceSpan, getInnermostNodeForTie: true);
63+
var propertySyntax = node.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
64+
var getter = propertySyntax.AccessorList?.Accessors.FirstOrDefault(i => i.Keyword.IsKind(SyntaxKind.GetKeyword));
65+
if (!(getter?.Body?.Statements.Count == 1))
66+
continue;
67+
68+
ReturnStatementSyntax returnStatement = getter?.Body?.Statements.FirstOrDefault() as ReturnStatementSyntax;
69+
ExpressionSyntax returnExpression = returnStatement.Expression;
70+
if (returnExpression.IsKind(SyntaxKind.SimpleMemberAccessExpression))
71+
{
72+
MemberAccessExpressionSyntax memberAccess = (MemberAccessExpressionSyntax)returnExpression;
73+
if (!memberAccess.Expression.IsKind(SyntaxKind.ThisExpression))
74+
continue;
75+
}
76+
77+
SemanticModel semanticModel = context.Compilation.GetSemanticModel(tree);
78+
SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(returnExpression, context.CancellationToken);
79+
IFieldSymbol fieldSymbol = symbolInfo.Symbol as IFieldSymbol;
80+
if (fieldSymbol.ContainingType != propertySymbol.ContainingType)
81+
continue;
82+
83+
if (!fieldSymbol.GetAttributes().Any(i => string.Equals("global::Newtonsoft.Json.JsonPropertyAttribute", i.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), StringComparison.Ordinal)))
84+
continue;
85+
86+
DocumentationCommentTriviaSyntax documentationCommentSyntax = propertySyntax.GetDocumentationCommentTriviaSyntax();
87+
if (documentationCommentSyntax == null)
88+
continue;
89+
90+
XmlNodeSyntax valueNode = documentationCommentSyntax.Content.GetFirstXmlElement("value");
91+
if (valueNode == null)
92+
continue;
93+
94+
string defaultValueToken = "NullIfNotIncluded";
95+
ITypeSymbol propertyType = propertySymbol.Type;
96+
if (propertyType.IsImmutableArray())
97+
defaultValueToken = "DefaultArrayIfNotIncluded";
98+
99+
XmlElementSyntax valueElementSyntax = valueNode as XmlElementSyntax;
100+
if (valueElementSyntax != null)
101+
{
102+
bool foundToken = false;
103+
foreach (XmlElementSyntax valueElementChild in valueElementSyntax.Content.OfType<XmlElementSyntax>())
104+
{
105+
if (!string.Equals("token", valueElementChild.StartTag?.Name?.ToString(), StringComparison.Ordinal))
106+
continue;
107+
108+
if (!string.Equals(defaultValueToken, valueElementChild.Content.ToFullString()))
109+
continue;
110+
111+
foundToken = true;
112+
break;
113+
}
114+
115+
if (foundToken)
116+
continue;
117+
}
118+
119+
string propertyTypeName = propertyType.ToMinimalDisplayString(semanticModel, returnExpression.SpanStart, SymbolDisplayFormat.CSharpErrorMessageFormat);
120+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, valueNode.GetLocation(), propertyTypeName, defaultValueToken));
121+
}
122+
}
123+
}
124+
}

OpenStackNetAnalyzers/OpenStackNetAnalyzers/OpenStackNetAnalyzers.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<Compile Include="DocumentationSyntaxExtensions.cs" />
3838
<Compile Include="DocumentDelegatingApiCallAnalyzer.cs" />
3939
<Compile Include="DocumentDelegatingApiCallCodeFix.cs" />
40+
<Compile Include="DocumentNullJsonValueAnalyzer.cs" />
4041
<Compile Include="DocumentValueFromSummaryAnalyzer.cs" />
4142
<Compile Include="DocumentValueFromSummaryCodeFix.cs" />
4243
<Compile Include="ImplementBuilderPatternAnalyzer.cs" />

0 commit comments

Comments
 (0)