Skip to content

Commit de7c2cf

Browse files
authored
Refactor metadata attribute support and add support for [EnumMember] (#163)
* Add support for [EnumMember] and change defaults * Add integration tests * Fix typo * Fix build * Make sure we don't overwrite the value if it's set
1 parent d3fa4a8 commit de7c2cf

File tree

76 files changed

+5921
-2957
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+5921
-2957
lines changed

.github/workflows/BuildAndPack.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ jobs:
6464
.nuke/temp
6565
~/.nuget/packages
6666
!~/.nuget/packages/netescapades.enumgenerators
67-
!~/.nuget/packages/netescapades.enumgenerators.attributes
6867
!~/.nuget/packages/netescapades.enumgenerators.interceptors
69-
!~/.nuget/packages/netescapades.enumgenerators.interceptors.attributes
7068
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }}
7169

7270
- name: Run './build.cmd Clean Test TestPackage PushToNuGet

build/Build.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ class Build : NukeBuild
101101
Target TestPackage => _ => _
102102
.DependsOn(Pack)
103103
.After(Test)
104-
.Produces(ArtifactsDirectory)
105104
.Executes(() =>
106105
{
107106
var projectFiles = new[]

src/NetEscapades.EnumGenerators.Attributes/EnumExtensionsAttribute.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@ public class EnumExtensionsAttribute : System.Attribute
99
{
1010
/// <summary>
1111
/// The namespace to generate the extension class.
12-
/// If not provided, the namespace of the enum will be used
12+
/// If not provided, the namespace of the enum will be used.
1313
/// </summary>
1414
public string? ExtensionClassNamespace { get; set; }
1515

1616
/// <summary>
1717
/// The name to use for the extension class.
18-
/// If not provided, the enum name with ""Extensions"" will be used.
19-
/// For example for an Enum called StatusCodes, the default name
20-
/// will be StatusCodesExtensions
18+
/// If not provided, the enum name with an <c>Extensions</c> suffix will be used.
19+
/// For example for an Enum called <c>StatusCodes</c>, the default name
20+
/// will be <c>StatusCodesExtensions</c>.
2121
/// </summary>
2222
public string? ExtensionClassName { get; set; }
2323

24+
/// <summary>
25+
/// The metadata source to use when serializing and deserializing using
26+
/// <c>ToStringFast()</c> and <c>TryParse()</c>. If not provided
27+
/// <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> will be
28+
/// used to provide the values. Alternatively, you can disable this feature
29+
/// entirely by using <see cref="EnumGenerators.MetadataSource.None"/>.
30+
/// </summary>
31+
public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute;
32+
2433
/// <summary>
2534
/// By default, when used with NetEscapades.EnumGenerators.Interceptors
2635
/// any interceptable usages of the enum will be replaced by usages of
@@ -48,11 +57,20 @@ public class EnumExtensionsAttribute<T> : System.Attribute
4857
/// <summary>
4958
/// The name to use for the extension class.
5059
/// If not provided, the enum name with an <c>Extensions</c> suffix will be used.
51-
/// For example for an Enum called StatusCodes, the default name
52-
/// will be StatusCodesExtensions.
60+
/// For example for an Enum called <c>StatusCodes</c>, the default name
61+
/// will be <c>StatusCodesExtensions</c>.
5362
/// </summary>
5463
public string? ExtensionClassName { get; set; }
5564

65+
/// <summary>
66+
/// The metadata source to use when serializing and deserializing using
67+
/// <c>ToStringFast()</c> and <c>TryParse()</c>. If not provided, the
68+
/// <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> will be
69+
/// used to provide the values. Alternatively, you can disable this feature
70+
/// entirely by using <see cref="EnumGenerators.MetadataSource.None"/>.
71+
/// </summary>
72+
public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute;
73+
5674
/// <summary>
5775
/// By default, when used with NetEscapades.EnumGenerators.Interceptors
5876
/// any interceptable usages of the enum will be replaced by usages of
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace NetEscapades.EnumGenerators
2+
{
3+
/// <summary>
4+
/// Defines where to obtain metadata for serializing and deserializing the enum
5+
/// </summary>
6+
public enum MetadataSource
7+
{
8+
/// <summary>
9+
/// Don't use attributes applied to enum members as a source of metadata for
10+
/// <c>ToStringFast()</c> and <c>TryParse()</c>. The name of the enum member
11+
/// will always be used for serialization.
12+
/// </summary>
13+
None,
14+
15+
/// <summary>
16+
/// Use values provided in <c>System.ComponentModel.DataAnnotations.DisplayAttribute</c> for
17+
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
18+
/// The value of the attribute will be used if available, otherwise the
19+
/// name of the enum member will be used for serialization.
20+
/// </summary>
21+
DisplayAttribute,
22+
23+
/// <summary>
24+
/// Use values provided in <see cref="System.ComponentModel.DescriptionAttribute"/> for
25+
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
26+
/// The value of the attribute will be used if available, otherwise the
27+
/// name of the enum member will be used for serialization.
28+
/// </summary>
29+
DescriptionAttribute,
30+
31+
/// <summary>
32+
/// Use values provided in <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> for
33+
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
34+
/// The value of the attribute will be used if available, otherwise the
35+
/// name of the enum member will be used for serialization.
36+
/// </summary>
37+
EnumMemberAttribute,
38+
}
39+
}

src/NetEscapades.EnumGenerators/Attributes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ internal static class Attributes
44
{
55
public const string DisplayAttribute = "System.ComponentModel.DataAnnotations.DisplayAttribute";
66
public const string DescriptionAttribute = "System.ComponentModel.DescriptionAttribute";
7+
public const string EnumMemberAttribute = "System.Runtime.Serialization.EnumMemberAttribute";
8+
79
public const string EnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute";
810
public const string ExternalEnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute`1";
911
public const string FlagsAttribute = "System.FlagsAttribute";

src/NetEscapades.EnumGenerators/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ namespace NetEscapades.EnumGenerators;
33
public static class Constants
44
{
55
public const string Version = "1.0.0-beta14";
6+
public const string MetadataSourcePropertyName = "EnumGenerator_EnumMetadataSource";
67
}

src/NetEscapades.EnumGenerators/Diagnostics/DuplicateExtensionClassAnalyzer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ public override void Initialize(AnalysisContext context)
4848
Location? location = null;
4949
string? ns = null;
5050
string? name = null;
51+
MetadataSource? source = null;
5152
foreach (var attributeData in enumSymbol.GetAttributes())
5253
{
5354
if (ct.IsCancellationRequested)
5455
{
5556
return;
5657
}
5758

58-
if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name))
59+
if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name, ref source))
5960
{
6061
location = attributeData.ApplicationSyntaxReference?.GetSyntax(ct).GetLocation()
6162
?? enumSymbol.Locations[0];

src/NetEscapades.EnumGenerators/EnumGenerator.cs

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis;
44
using Microsoft.CodeAnalysis.CSharp;
55
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
67
using Microsoft.CodeAnalysis.Operations;
78
using Microsoft.CodeAnalysis.Text;
89

@@ -13,11 +14,16 @@ public class EnumGenerator : IIncrementalGenerator
1314
{
1415
public void Initialize(IncrementalGeneratorInitializationContext context)
1516
{
17+
var defaultMetadataSource = context.AnalyzerConfigOptionsProvider
18+
.Select(GetDefaultMetadataSource);
19+
1620
var csharp14IsSupported = context.CompilationProvider
1721
.Select((x,_) => x is CSharpCompilation
1822
{
1923
LanguageVersion: LanguageVersion.Preview or >= (LanguageVersion)1400 // C#14
2024
});
25+
26+
var defaults = csharp14IsSupported.Combine(defaultMetadataSource);
2127

2228
IncrementalValuesProvider<EnumToGenerate> enumsToGenerate = context.SyntaxProvider
2329
.ForAttributeWithMetadataName(Attributes.EnumExtensionsAttribute,
@@ -37,16 +43,35 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3743
.SelectMany(static (m, _) => m!.Value)
3844
.WithTrackingName(TrackingNames.InitialExternalExtraction);
3945

40-
context.RegisterSourceOutput(enumsToGenerate.Combine(csharp14IsSupported),
41-
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right, spc));
46+
context.RegisterSourceOutput(enumsToGenerate.Combine(defaults),
47+
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right.Left, enumToGenerate.Right.Right, spc));
4248

43-
context.RegisterSourceOutput(externalEnums.Combine(csharp14IsSupported),
44-
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right, spc));
49+
context.RegisterSourceOutput(externalEnums.Combine(defaults),
50+
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right.Left, enumToGenerate.Right.Right, spc));
4551
}
4652

47-
static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported, SourceProductionContext context)
53+
private static MetadataSource GetDefaultMetadataSource(AnalyzerConfigOptionsProvider configOptions, CancellationToken ct)
4854
{
49-
var (result, filename) = SourceGenerationHelper.GenerateExtensionClass(in enumToGenerate, csharp14IsSupported);
55+
const MetadataSource defaultValue = MetadataSource.EnumMemberAttribute;
56+
if (configOptions.GlobalOptions.TryGetValue($"build_property.{Constants.MetadataSourcePropertyName}",
57+
out var source))
58+
{
59+
return source switch
60+
{
61+
nameof(MetadataSource.None) => MetadataSource.None,
62+
nameof(MetadataSource.DisplayAttribute) => MetadataSource.DisplayAttribute,
63+
nameof(MetadataSource.DescriptionAttribute) => MetadataSource.DescriptionAttribute,
64+
nameof(MetadataSource.EnumMemberAttribute) => MetadataSource.EnumMemberAttribute,
65+
_ => defaultValue,
66+
};
67+
}
68+
69+
return defaultValue;
70+
}
71+
72+
static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported, MetadataSource source, SourceProductionContext context)
73+
{
74+
var (result, filename) = SourceGenerationHelper.GenerateExtensionClass(in enumToGenerate, csharp14IsSupported, source);
5075
context.AddSource(filename, SourceText.From(result, Encoding.UTF8));
5176
}
5277

@@ -74,6 +99,7 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
7499
bool hasFlags = false;
75100
string? name = null;
76101
string? nameSpace = null;
102+
MetadataSource? source = null;
77103

78104
foreach (KeyValuePair<string, TypedConstant> namedArgument in attribute.NamedArguments)
79105
{
@@ -89,6 +115,12 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
89115
{
90116
name = n;
91117
}
118+
119+
if (namedArgument.Key == "MetadataSource"
120+
&& namedArgument.Value is { Kind: TypedConstantKind.Enum, Value: { } ms })
121+
{
122+
source = (MetadataSource)(int)ms;
123+
}
92124
}
93125

94126
foreach (var attrData in enumSymbol.GetAttributes())
@@ -102,7 +134,7 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
102134
}
103135
}
104136

105-
var enumToGenerate = TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
137+
var enumToGenerate = TryExtractEnumSymbol(enumSymbol, name, nameSpace, source, hasFlags);
106138
if (enumToGenerate is not null)
107139
{
108140
enums ??= new();
@@ -135,6 +167,7 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
135167
var hasFlags = false;
136168
string? nameSpace = null;
137169
string? name = null;
170+
MetadataSource? metadataSource = null;
138171

139172
foreach (AttributeData attributeData in enumSymbol.GetAttributes())
140173
{
@@ -146,13 +179,17 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
146179
continue;
147180
}
148181

149-
TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name);
182+
TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name, ref metadataSource);
150183
}
151184

152-
return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
185+
return TryExtractEnumSymbol(enumSymbol, name, nameSpace, metadataSource, hasFlags);
153186
}
154187

155-
internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData, ref string? nameSpace, ref string? name)
188+
internal static bool TryGetExtensionAttributeDetails(
189+
AttributeData attributeData,
190+
ref string? nameSpace,
191+
ref string? name,
192+
ref MetadataSource? source)
156193
{
157194
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
158195
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
@@ -174,6 +211,12 @@ internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData
174211
{
175212
name = n;
176213
}
214+
215+
if (namedArgument.Key == "MetadataSource"
216+
&& namedArgument.Value is { Kind: TypedConstantKind.Enum, Value: { } ms })
217+
{
218+
source = (MetadataSource)(int)ms;
219+
}
177220
}
178221

179222
return true;
@@ -185,7 +228,12 @@ internal static string GetEnumExtensionNamespace(INamedTypeSymbol enumSymbol)
185228
internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
186229
=> enumSymbol.Name + "Extensions";
187230

188-
static EnumToGenerate? TryExtractEnumSymbol(INamedTypeSymbol enumSymbol, string? name, string? nameSpace, bool hasFlags)
231+
static EnumToGenerate? TryExtractEnumSymbol(
232+
INamedTypeSymbol enumSymbol,
233+
string? name,
234+
string? nameSpace,
235+
MetadataSource? metadataSource,
236+
bool hasFlags)
189237
{
190238
name ??= GetEnumExtensionName(enumSymbol);
191239
nameSpace ??= GetEnumExtensionNamespace(enumSymbol);
@@ -195,8 +243,6 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
195243

196244
var enumMembers = enumSymbol.GetMembers();
197245
var members = new List<(string, EnumValueOption)>(enumMembers.Length);
198-
HashSet<string>? displayNames = null;
199-
var isDisplayNameTheFirstPresence = false;
200246

201247
foreach (var member in enumMembers)
202248
{
@@ -206,6 +252,8 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
206252
}
207253

208254
string? displayName = null;
255+
string? description = null;
256+
string? enumMemberValue = null;
209257
foreach (var attribute in member.GetAttributes())
210258
{
211259
if (attribute.AttributeClass?.Name == "DisplayAttribute" &&
@@ -215,9 +263,7 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
215263
{
216264
if (namedArgument.Key == "Name" && namedArgument.Value.Value?.ToString() is { } dn)
217265
{
218-
// found display attribute, all done
219266
displayName = dn;
220-
goto addDisplayName;
221267
}
222268
}
223269
}
@@ -228,22 +274,24 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
228274
{
229275
if (attribute.ConstructorArguments[0].Value?.ToString() is { } dn)
230276
{
231-
// found display attribute, all done
232-
// Handle cases where contains a quote or a backslash
233-
displayName = dn;
234-
goto addDisplayName;
277+
description = dn;
235278
}
236279
}
237-
}
238280

239-
addDisplayName:
240-
if (displayName is not null)
241-
{
242-
displayNames ??= new();
243-
isDisplayNameTheFirstPresence = displayNames.Add(displayName);
281+
if (attribute.AttributeClass?.Name == "EnumMemberAttribute" &&
282+
attribute.AttributeClass.ToDisplayString() == Attributes.EnumMemberAttribute)
283+
{
284+
foreach (var namedArgument in attribute.NamedArguments)
285+
{
286+
if (namedArgument.Key == "Value" && namedArgument.Value.Value?.ToString() is { } dn)
287+
{
288+
enumMemberValue = dn;
289+
}
290+
}
291+
}
244292
}
245-
246-
members.Add((member.Name, new EnumValueOption(displayName, isDisplayNameTheFirstPresence, constantValue)));
293+
294+
members.Add((member.Name, new EnumValueOption(displayName, description, enumMemberValue, constantValue)));
247295
}
248296

249297
return new EnumToGenerate(
@@ -254,7 +302,7 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
254302
isPublic: enumSymbol.DeclaredAccessibility == Accessibility.Public,
255303
hasFlags: hasFlags,
256304
names: members,
257-
isDisplayAttributeUsed: displayNames?.Count > 0);
305+
metadataSource: metadataSource);
258306
}
259307

260308
}

0 commit comments

Comments
 (0)