Skip to content

Commit b4db198

Browse files
Copilotandrewlock
andcommitted
Add IncorrectMetadataAttributeAnalyzer and code fix provider with tests
Co-authored-by: andrewlock <18755388+andrewlock@users.noreply.github.com>
1 parent 412e1b9 commit b4db198

File tree

3 files changed

+774
-0
lines changed

3 files changed

+774
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
8+
namespace NetEscapades.EnumGenerators.Diagnostics;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class IncorrectMetadataAttributeAnalyzer : DiagnosticAnalyzer
12+
{
13+
public const string DiagnosticId = "NEEG004";
14+
public static readonly DiagnosticDescriptor Rule = new(
15+
#pragma warning disable RS2008 // Enable Analyzer Release Tracking
16+
id: DiagnosticId,
17+
#pragma warning restore RS2008
18+
title: "Metadata attribute will be ignored",
19+
messageFormat: "The '{0}' attribute on enum member '{1}' will be ignored because the enum is configured to use '{2}'",
20+
category: "Usage",
21+
defaultSeverity: DiagnosticSeverity.Info,
22+
isEnabledByDefault: true);
23+
24+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
25+
=> ImmutableArray.Create(Rule);
26+
27+
public override void Initialize(AnalysisContext context)
28+
{
29+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
30+
context.EnableConcurrentExecution();
31+
context.RegisterSyntaxNodeAction(AnalyzeEnumDeclaration, SyntaxKind.EnumDeclaration);
32+
}
33+
34+
private static MetadataSource GetDefaultMetadataSource(AnalyzerOptions options)
35+
{
36+
const MetadataSource defaultValue = MetadataSource.EnumMemberAttribute;
37+
38+
if (options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(
39+
$"build_property.{Constants.MetadataSourcePropertyName}",
40+
out var source))
41+
{
42+
return source switch
43+
{
44+
nameof(MetadataSource.None) => MetadataSource.None,
45+
nameof(MetadataSource.DisplayAttribute) => MetadataSource.DisplayAttribute,
46+
nameof(MetadataSource.DescriptionAttribute) => MetadataSource.DescriptionAttribute,
47+
nameof(MetadataSource.EnumMemberAttribute) => MetadataSource.EnumMemberAttribute,
48+
_ => defaultValue,
49+
};
50+
}
51+
52+
return defaultValue;
53+
}
54+
55+
private static void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context)
56+
{
57+
// Get the default metadata source from MSBuild properties
58+
var defaultMetadataSource = GetDefaultMetadataSource(context.Options);
59+
var enumDeclaration = (EnumDeclarationSyntax)context.Node;
60+
61+
// Check if enum has [EnumExtensions] attribute
62+
AttributeSyntax? enumExtensionsAttribute = null;
63+
MetadataSource? explicitMetadataSource = null;
64+
65+
foreach (var attributeList in enumDeclaration.AttributeLists)
66+
{
67+
foreach (var attribute in attributeList.Attributes)
68+
{
69+
// Check attribute name syntactically first
70+
var attributeName = attribute.Name.ToString();
71+
if (attributeName == "EnumExtensions" || attributeName == "EnumExtensionsAttribute")
72+
{
73+
// Verify with semantic model for precision
74+
var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute);
75+
if (symbolInfo.Symbol is IMethodSymbol method &&
76+
method.ContainingType.ToDisplayString() == Attributes.EnumExtensionsAttribute)
77+
{
78+
enumExtensionsAttribute = attribute;
79+
80+
// Check if MetadataSource is explicitly set
81+
if (attribute.ArgumentList is not null)
82+
{
83+
foreach (var arg in attribute.ArgumentList.Arguments)
84+
{
85+
if (arg.NameEquals?.Name.Identifier.Text == "MetadataSource")
86+
{
87+
// Try to get the metadata source value
88+
var attrData = context.SemanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType;
89+
foreach (var attrDataItem in context.SemanticModel.GetDeclaredSymbol(enumDeclaration)?.GetAttributes() ?? Enumerable.Empty<AttributeData>())
90+
{
91+
if (attrDataItem.AttributeClass?.ToDisplayString() == Attributes.EnumExtensionsAttribute)
92+
{
93+
foreach (var namedArg in attrDataItem.NamedArguments)
94+
{
95+
if (namedArg.Key == "MetadataSource" && namedArg.Value.Value is int metadataSourceValue)
96+
{
97+
explicitMetadataSource = (MetadataSource)metadataSourceValue;
98+
break;
99+
}
100+
}
101+
}
102+
}
103+
break;
104+
}
105+
}
106+
}
107+
break;
108+
}
109+
}
110+
}
111+
112+
if (enumExtensionsAttribute is not null)
113+
{
114+
break;
115+
}
116+
}
117+
118+
if (enumExtensionsAttribute is null)
119+
{
120+
return;
121+
}
122+
123+
// Determine the effective metadata source
124+
var effectiveMetadataSource = explicitMetadataSource ?? defaultMetadataSource;
125+
126+
// If MetadataSource is None, no attributes will be used, so no need to warn
127+
if (effectiveMetadataSource == MetadataSource.None)
128+
{
129+
return;
130+
}
131+
132+
// Get the enum symbol
133+
var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclaration);
134+
if (enumSymbol is null)
135+
{
136+
return;
137+
}
138+
139+
// Track which metadata attributes are found
140+
bool hasCorrectAttribute = false;
141+
var incorrectAttributes = new System.Collections.Generic.List<(Location Location, string AttributeName, string MemberName)>();
142+
143+
// Analyze each enum member
144+
foreach (var member in enumSymbol.GetMembers().OfType<IFieldSymbol>())
145+
{
146+
if (!member.IsConst)
147+
{
148+
continue;
149+
}
150+
151+
foreach (var attribute in member.GetAttributes())
152+
{
153+
var attributeType = attribute.AttributeClass?.ToDisplayString();
154+
155+
if (attributeType == Attributes.DisplayAttribute)
156+
{
157+
if (effectiveMetadataSource == MetadataSource.DisplayAttribute)
158+
{
159+
hasCorrectAttribute = true;
160+
}
161+
else
162+
{
163+
var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation();
164+
if (location is not null)
165+
{
166+
incorrectAttributes.Add((location, "Display", member.Name));
167+
}
168+
}
169+
}
170+
else if (attributeType == Attributes.DescriptionAttribute)
171+
{
172+
if (effectiveMetadataSource == MetadataSource.DescriptionAttribute)
173+
{
174+
hasCorrectAttribute = true;
175+
}
176+
else
177+
{
178+
var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation();
179+
if (location is not null)
180+
{
181+
incorrectAttributes.Add((location, "Description", member.Name));
182+
}
183+
}
184+
}
185+
else if (attributeType == Attributes.EnumMemberAttribute)
186+
{
187+
if (effectiveMetadataSource == MetadataSource.EnumMemberAttribute)
188+
{
189+
hasCorrectAttribute = true;
190+
}
191+
else
192+
{
193+
var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation();
194+
if (location is not null)
195+
{
196+
incorrectAttributes.Add((location, "EnumMember", member.Name));
197+
}
198+
}
199+
}
200+
}
201+
}
202+
203+
// Only report diagnostics if we found incorrect attributes and no correct attributes
204+
if (incorrectAttributes.Count > 0 && !hasCorrectAttribute)
205+
{
206+
var effectiveSourceName = effectiveMetadataSource switch
207+
{
208+
MetadataSource.DisplayAttribute => "DisplayAttribute",
209+
MetadataSource.DescriptionAttribute => "DescriptionAttribute",
210+
MetadataSource.EnumMemberAttribute => "EnumMemberAttribute",
211+
_ => "None"
212+
};
213+
214+
foreach (var (location, attributeName, memberName) in incorrectAttributes)
215+
{
216+
var diagnostic = Diagnostic.Create(
217+
Rule,
218+
location,
219+
attributeName,
220+
memberName,
221+
effectiveSourceName);
222+
223+
context.ReportDiagnostic(diagnostic);
224+
}
225+
}
226+
}
227+
}

0 commit comments

Comments
 (0)