Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/BuildAndPack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ jobs:
.nuke/temp
~/.nuget/packages
!~/.nuget/packages/netescapades.enumgenerators
!~/.nuget/packages/netescapades.enumgenerators.attributes
!~/.nuget/packages/netescapades.enumgenerators.interceptors
!~/.nuget/packages/netescapades.enumgenerators.interceptors.attributes
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }}

- name: Run './build.cmd Clean Test TestPackage PushToNuGet
Expand Down
1 change: 0 additions & 1 deletion build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ class Build : NukeBuild
Target TestPackage => _ => _
.DependsOn(Pack)
.After(Test)
.Produces(ArtifactsDirectory)
.Executes(() =>
{
var projectFiles = new[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,27 @@ public class EnumExtensionsAttribute : System.Attribute
{
/// <summary>
/// The namespace to generate the extension class.
/// If not provided, the namespace of the enum will be used
/// If not provided, the namespace of the enum will be used.
/// </summary>
public string? ExtensionClassNamespace { get; set; }

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

/// <summary>
/// The metadata source to use when serializing and deserializing using
/// <c>ToStringFast()</c> and <c>TryParse()</c>. If not provided
/// <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> will be
/// used to provide the values. Alternatively, you can disable this feature
/// entirely by using <see cref="EnumGenerators.MetadataSource.None"/>.
/// </summary>
public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute;

/// <summary>
/// By default, when used with NetEscapades.EnumGenerators.Interceptors
/// any interceptable usages of the enum will be replaced by usages of
Expand Down Expand Up @@ -48,11 +57,20 @@ public class EnumExtensionsAttribute<T> : System.Attribute
/// <summary>
/// The name to use for the extension class.
/// If not provided, the enum name with an <c>Extensions</c> suffix will be used.
/// For example for an Enum called StatusCodes, the default name
/// will be StatusCodesExtensions.
/// For example for an Enum called <c>StatusCodes</c>, the default name
/// will be <c>StatusCodesExtensions</c>.
/// </summary>
public string? ExtensionClassName { get; set; }

/// <summary>
/// The metadata source to use when serializing and deserializing using
/// <c>ToStringFast()</c> and <c>TryParse()</c>. If not provided, the
/// <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> will be
/// used to provide the values. Alternatively, you can disable this feature
/// entirely by using <see cref="EnumGenerators.MetadataSource.None"/>.
/// </summary>
public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute;

/// <summary>
/// By default, when used with NetEscapades.EnumGenerators.Interceptors
/// any interceptable usages of the enum will be replaced by usages of
Expand Down
39 changes: 39 additions & 0 deletions src/NetEscapades.EnumGenerators.Attributes/MetadataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace NetEscapades.EnumGenerators
{
/// <summary>
/// Defines where to obtain metadata for serializing and deserializing the enum
/// </summary>
public enum MetadataSource
{
/// <summary>
/// Don't use attributes applied to enum members as a source of metadata for
/// <c>ToStringFast()</c> and <c>TryParse()</c>. The name of the enum member
/// will always be used for serialization.
/// </summary>
None,

/// <summary>
/// Use values provided in <c>System.ComponentModel.DataAnnotations.DisplayAttribute</c> for
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
/// The value of the attribute will be used if available, otherwise the
/// name of the enum member will be used for serialization.
/// </summary>
DisplayAttribute,

/// <summary>
/// Use values provided in <see cref="System.ComponentModel.DescriptionAttribute"/> for
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
/// The value of the attribute will be used if available, otherwise the
/// name of the enum member will be used for serialization.
/// </summary>
DescriptionAttribute,

/// <summary>
/// Use values provided in <see cref="System.Runtime.Serialization.EnumMemberAttribute"/> for
/// determining the value to use for <c>ToStringFast()</c> and <c>TryParse()</c>.
/// The value of the attribute will be used if available, otherwise the
/// name of the enum member will be used for serialization.
/// </summary>
EnumMemberAttribute,
}
}
2 changes: 2 additions & 0 deletions src/NetEscapades.EnumGenerators/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ internal static class Attributes
{
public const string DisplayAttribute = "System.ComponentModel.DataAnnotations.DisplayAttribute";
public const string DescriptionAttribute = "System.ComponentModel.DescriptionAttribute";
public const string EnumMemberAttribute = "System.Runtime.Serialization.EnumMemberAttribute";

public const string EnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute";
public const string ExternalEnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute`1";
public const string FlagsAttribute = "System.FlagsAttribute";
Expand Down
1 change: 1 addition & 0 deletions src/NetEscapades.EnumGenerators/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace NetEscapades.EnumGenerators;
public static class Constants
{
public const string Version = "1.0.0-beta14";
public const string MetadataSourcePropertyName = "EnumGenerator_EnumMetadataSource";
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ public override void Initialize(AnalysisContext context)
Location? location = null;
string? ns = null;
string? name = null;
MetadataSource? source = null;
foreach (var attributeData in enumSymbol.GetAttributes())
{
if (ct.IsCancellationRequested)
{
return;
}

if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name))
if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name, ref source))
{
location = attributeData.ApplicationSyntaxReference?.GetSyntax(ct).GetLocation()
?? enumSymbol.Locations[0];
Expand Down
104 changes: 76 additions & 28 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;

Expand All @@ -13,11 +14,16 @@ public class EnumGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var defaultMetadataSource = context.AnalyzerConfigOptionsProvider
.Select(GetDefaultMetadataSource);

var csharp14IsSupported = context.CompilationProvider
.Select((x,_) => x is CSharpCompilation
{
LanguageVersion: LanguageVersion.Preview or >= (LanguageVersion)1400 // C#14
});

var defaults = csharp14IsSupported.Combine(defaultMetadataSource);

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

context.RegisterSourceOutput(enumsToGenerate.Combine(csharp14IsSupported),
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right, spc));
context.RegisterSourceOutput(enumsToGenerate.Combine(defaults),
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right.Left, enumToGenerate.Right.Right, spc));

context.RegisterSourceOutput(externalEnums.Combine(csharp14IsSupported),
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right, spc));
context.RegisterSourceOutput(externalEnums.Combine(defaults),
static (spc, enumToGenerate) => Execute(in enumToGenerate.Left, enumToGenerate.Right.Left, enumToGenerate.Right.Right, spc));
}

static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported, SourceProductionContext context)
private static MetadataSource GetDefaultMetadataSource(AnalyzerConfigOptionsProvider configOptions, CancellationToken ct)
{
var (result, filename) = SourceGenerationHelper.GenerateExtensionClass(in enumToGenerate, csharp14IsSupported);
const MetadataSource defaultValue = MetadataSource.EnumMemberAttribute;
if (configOptions.GlobalOptions.TryGetValue($"build_property.{Constants.MetadataSourcePropertyName}",
out var source))
{
return source switch
{
nameof(MetadataSource.None) => MetadataSource.None,
nameof(MetadataSource.DisplayAttribute) => MetadataSource.DisplayAttribute,
nameof(MetadataSource.DescriptionAttribute) => MetadataSource.DescriptionAttribute,
nameof(MetadataSource.EnumMemberAttribute) => MetadataSource.EnumMemberAttribute,
_ => defaultValue,
};
}

return defaultValue;
}

static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported, MetadataSource source, SourceProductionContext context)
{
var (result, filename) = SourceGenerationHelper.GenerateExtensionClass(in enumToGenerate, csharp14IsSupported, source);
context.AddSource(filename, SourceText.From(result, Encoding.UTF8));
}

Expand Down Expand Up @@ -74,6 +99,7 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
bool hasFlags = false;
string? name = null;
string? nameSpace = null;
MetadataSource? source = null;

foreach (KeyValuePair<string, TypedConstant> namedArgument in attribute.NamedArguments)
{
Expand All @@ -89,6 +115,12 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
{
name = n;
}

if (namedArgument.Key == "MetadataSource"
&& namedArgument.Value is { Kind: TypedConstantKind.Enum, Value: { } ms })
{
source = (MetadataSource)(int)ms;
}
}

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

var enumToGenerate = TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
var enumToGenerate = TryExtractEnumSymbol(enumSymbol, name, nameSpace, source, hasFlags);
if (enumToGenerate is not null)
{
enums ??= new();
Expand Down Expand Up @@ -135,6 +167,7 @@ static void Execute(in EnumToGenerate enumToGenerate, bool csharp14IsSupported,
var hasFlags = false;
string? nameSpace = null;
string? name = null;
MetadataSource? metadataSource = null;

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

TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name);
TryGetExtensionAttributeDetails(attributeData, ref nameSpace, ref name, ref metadataSource);
}

return TryExtractEnumSymbol(enumSymbol, name, nameSpace, hasFlags);
return TryExtractEnumSymbol(enumSymbol, name, nameSpace, metadataSource, hasFlags);
}

internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData, ref string? nameSpace, ref string? name)
internal static bool TryGetExtensionAttributeDetails(
AttributeData attributeData,
ref string? nameSpace,
ref string? name,
ref MetadataSource? source)
{
if (attributeData.AttributeClass?.Name != "EnumExtensionsAttribute" ||
attributeData.AttributeClass.ToDisplayString() != Attributes.EnumExtensionsAttribute)
Expand All @@ -174,6 +211,12 @@ internal static bool TryGetExtensionAttributeDetails(AttributeData attributeData
{
name = n;
}

if (namedArgument.Key == "MetadataSource"
&& namedArgument.Value is { Kind: TypedConstantKind.Enum, Value: { } ms })
{
source = (MetadataSource)(int)ms;
}
}

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

static EnumToGenerate? TryExtractEnumSymbol(INamedTypeSymbol enumSymbol, string? name, string? nameSpace, bool hasFlags)
static EnumToGenerate? TryExtractEnumSymbol(
INamedTypeSymbol enumSymbol,
string? name,
string? nameSpace,
MetadataSource? metadataSource,
bool hasFlags)
{
name ??= GetEnumExtensionName(enumSymbol);
nameSpace ??= GetEnumExtensionNamespace(enumSymbol);
Expand All @@ -195,8 +243,6 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)

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

foreach (var member in enumMembers)
{
Expand All @@ -206,6 +252,8 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
}

string? displayName = null;
string? description = null;
string? enumMemberValue = null;
foreach (var attribute in member.GetAttributes())
{
if (attribute.AttributeClass?.Name == "DisplayAttribute" &&
Expand All @@ -215,9 +263,7 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
{
if (namedArgument.Key == "Name" && namedArgument.Value.Value?.ToString() is { } dn)
{
// found display attribute, all done
displayName = dn;
goto addDisplayName;
}
}
}
Expand All @@ -228,22 +274,24 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
{
if (attribute.ConstructorArguments[0].Value?.ToString() is { } dn)
{
// found display attribute, all done
// Handle cases where contains a quote or a backslash
displayName = dn;
goto addDisplayName;
description = dn;
}
}
}

addDisplayName:
if (displayName is not null)
{
displayNames ??= new();
isDisplayNameTheFirstPresence = displayNames.Add(displayName);
if (attribute.AttributeClass?.Name == "EnumMemberAttribute" &&
attribute.AttributeClass.ToDisplayString() == Attributes.EnumMemberAttribute)
{
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Key == "Value" && namedArgument.Value.Value?.ToString() is { } dn)
{
enumMemberValue = dn;
}
}
}
}
members.Add((member.Name, new EnumValueOption(displayName, isDisplayNameTheFirstPresence, constantValue)));

members.Add((member.Name, new EnumValueOption(displayName, description, enumMemberValue, constantValue)));
}

return new EnumToGenerate(
Expand All @@ -254,7 +302,7 @@ internal static string GetEnumExtensionName(INamedTypeSymbol enumSymbol)
isPublic: enumSymbol.DeclaredAccessibility == Accessibility.Public,
hasFlags: hasFlags,
names: members,
isDisplayAttributeUsed: displayNames?.Count > 0);
metadataSource: metadataSource);
}

}
Loading
Loading