├── global.json ├── icon.png ├── icon_32.png ├── .nuke └── parameters.json ├── tests ├── NetEscapades.EnumGenerators.IntegrationTests │ ├── xunit.runner.json │ ├── .editorconfig │ ├── EnumParseOptionsTests.cs │ ├── SerializationOptionsTests.cs │ ├── AnalyzerTests.cs │ ├── NetEscapades.EnumGenerators.IntegrationTests.csproj │ ├── EnumInFooExtensionEverythingTests.cs │ ├── EnumInFooExtensionsTests.cs │ ├── LongEnumExtensionsTests.cs │ ├── EnumInNamespaceExtensionsTests.cs │ ├── ExternalEnumExtensionsTests.cs │ ├── EnumWithReservedKeywordsExtensionsTests.cs │ ├── EnumWithSameDisplayNameExtensionsTests.cs │ ├── EnumWithEnumMemberInNamespaceExtensionsTests.cs │ ├── EnumWithDescriptionInNamespaceExtensionsTests.cs │ ├── EnumWithDisplayNameInNamespaceExtensionsTests.cs │ ├── EnumWithNoMetadataSourcesInNamespaceExtensionsTests.cs │ ├── FlagsEnumExtensionsTests.cs │ ├── ExternalFlagsEnumExtensionsTests.cs │ └── Enums.cs ├── NetEscapades.EnumGenerators.Benchmarks │ ├── Directory.Build.props │ ├── NetEscapades.EnumGenerators.Benchmarks.csproj │ └── EnumHelper.cs ├── NetEscapades.EnumGenerators.Interceptors.IntegrationTests │ ├── xunit.runner.json │ ├── Enums.cs │ ├── NetEscapades.EnumGenerators.Interceptors.IntegrationTests.csproj │ └── InterceptorTests.cs ├── NetEscapades.EnumGenerators.Tests │ ├── NetEscapades.EnumGenerators.Tests.Roslyn4_04.csproj │ ├── NetEscapades.EnumGenerators.Tests.Project.targets │ ├── EquatableArrayTests.cs │ ├── NetEscapades.EnumGenerators.Tests.csproj │ └── SourceGenerationHelperSnapshotTests.cs ├── NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests │ ├── InterceptionAttributes.cs │ └── NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests.csproj ├── NetEscapades.EnumGenerators.Nuget.NetStandard │ └── NetEscapades.EnumGenerators.Nuget.NetStandard.csproj ├── NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory │ └── NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.csproj ├── NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests │ ├── InterceptionAttributes.cs │ └── NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests.csproj ├── NetEscapades.EnumGenerators.NetStandard │ └── NetEscapades.EnumGenerators.NetStandard.csproj ├── NetEscapades.EnumGenerators.NetStandard.SystemMemory │ └── NetEscapades.EnumGenerators.NetStandard.SystemMemory.csproj ├── Directory.Build.props ├── NetEscapades.EnumGenerators.NetStandard.IntegrationTests │ └── NetEscapades.EnumGenerators.NetStandard.IntegrationTests.csproj ├── NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests │ └── NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests.csproj ├── NetEscapades.EnumGenerators.Nuget.IntegrationTests │ ├── NetEscapades.EnumGenerators.Nuget.IntegrationTests.csproj │ └── NetEscapades.EnumGenerators.Nuget.IntegrationTests.Project.targets ├── NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests │ └── NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests.csproj └── NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.IntegrationTests │ └── NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.IntegrationTests.csproj ├── src ├── NetEscapades.EnumGenerators │ ├── GlobalUsings.cs │ ├── Constants.cs │ ├── TrackingNames.cs │ ├── NetEscapades.EnumGenerators.props │ ├── SymbolHelpers.cs │ ├── NetEscapades.EnumGenerators.targets │ ├── Attributes.cs │ ├── LocationInfo.cs │ ├── EnumToGenerate.cs │ ├── EnumValueOption.cs │ ├── NetEscapades.EnumGenerators.csproj │ ├── Diagnostics │ │ ├── HasFlagCodeFixProvider.cs │ │ ├── EnumInGenericTypeAnalyzer.cs │ │ ├── HasFlagAnalyzer.cs │ │ ├── AnalyzerHelpers.cs │ │ ├── DuplicateEnumValueAnalyzer.cs │ │ └── DuplicateExtensionClassAnalyzer.cs │ └── EquatableArray.cs ├── NetEscapades.EnumGenerators.Interceptors │ ├── NetEscapades.EnumGenerators.Interceptors.props │ ├── Constants.cs │ ├── NetEscapades.EnumGenerators.Interceptors.targets │ ├── DiagnosticHelper.cs │ ├── LocationInfo.cs │ ├── TrackingNames.cs │ ├── EnumToIntercept.cs │ ├── NetEscapades.EnumGenerators.Interceptors.csproj │ ├── EquatableArray.cs │ ├── SourceGenerationHelper.cs │ └── README.md ├── NetEscapades.EnumGenerators.Attributes │ ├── NetEscapades.EnumGenerators.Attributes.csproj │ ├── SerializationTransform.cs │ ├── SerializationOptions.cs │ ├── MetadataSource.cs │ ├── EnumParseOptions.cs │ └── EnumExtensionsAttribute.cs └── NetEscapades.EnumGenerators.Interceptors.Attributes │ ├── NetEscapades.EnumGenerators.Interceptors.Attributes.csproj │ └── InterceptableAttribute.cs ├── NuGet.integration-tests.config ├── version.props ├── releasenotes.props ├── samples └── ToStringFastExample │ ├── Program.cs │ └── ToStringFastExample.csproj ├── LICENSE ├── Directory.Build.props ├── .gitattributes ├── .github └── workflows │ └── BuildAndPack.yml └── .gitignore /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": true 4 | } 5 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/HEAD/icon.png -------------------------------------------------------------------------------- /icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/HEAD/icon_32.png -------------------------------------------------------------------------------- /.nuke/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./build.schema.json", 3 | "Solution": "NetEscapades.EnumGenerators.sln" 4 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "shadowCopy": false 4 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Benchmarks/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Interceptors.IntegrationTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "shadowCopy": false 4 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using ExternalEnumDictionary = System.Collections.Generic.Dictionary; -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /NuGet.integration-tests.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/.editorconfig: -------------------------------------------------------------------------------- 1 | #### .NET Coding Conventions #### 2 | [*.{cs,vb}] 3 | 4 | dotnet_diagnostic.NEEG001.severity = error 5 | dotnet_diagnostic.NEEG002.severity = error 6 | dotnet_diagnostic.NEEG003.severity = error 7 | dotnet_diagnostic.NEEG004.severity = error 8 | dotnet_diagnostic.NEEG005.severity = error -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators.Interceptors; 2 | 3 | internal static class Constants 4 | { 5 | public const string EnabledPropertyName = "EnableEnumGeneratorInterceptor"; 6 | public const string InterceptableAttribute = "NetEscapades.EnumGenerators.InterceptableAttribute`1"; 7 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | public static class Constants 4 | { 5 | public const string Version = "1.0.0-beta16"; 6 | public const string MetadataSourcePropertyName = "EnumGenerator_EnumMetadataSource"; 7 | public const string ForceExtensionMembers = "EnumGenerator_ForceExtensionMembers"; 8 | } -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0 4 | beta16 5 | $(VersionPrefix) 6 | $(VersionPrefix)-$(VersionSuffix) 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/NetEscapades.EnumGenerators.Attributes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net451 5 | enable 6 | NetEscapades.EnumGenerators 7 | false 8 | true 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/TrackingNames.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | /// 4 | /// Names that are attached to incremental generator stages for tracking 5 | /// 6 | public class TrackingNames 7 | { 8 | public const string InitialExtraction = nameof(InitialExtraction); 9 | public const string InitialExternalExtraction = nameof(InitialExternalExtraction); 10 | public const string RemovingNulls = nameof(RemovingNulls); 11 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors.Attributes/NetEscapades.EnumGenerators.Interceptors.Attributes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net451 5 | enable 6 | NetEscapades.EnumGenerators 7 | false 8 | true 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Tests/NetEscapades.EnumGenerators.Tests.Roslyn4_04.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.04.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /releasenotes.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | $(PackageReleaseNotes) 12 | See $(PackageProjectUrl)/blob/main/CHANGELOG.md#v$(PackageVersion.Replace('.','')) for more details. 13 | 14 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(InterceptorsNamespaces);NetEscapades.EnumGenerators 5 | $(InterceptorsPreviewNamespaces);NetEscapades.EnumGenerators 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | EnumMemberAttribute 4 | false 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/SymbolHelpers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace NetEscapades.EnumGenerators; 4 | 5 | internal static class SymbolHelpers 6 | { 7 | public static bool IsNestedInGenericType(INamedTypeSymbol enumSymbol) 8 | { 9 | var containingType = enumSymbol.ContainingType; 10 | while (containingType is not null) 11 | { 12 | if (containingType.IsGenericType) 13 | { 14 | return true; 15 | } 16 | containingType = containingType.ContainingType; 17 | } 18 | return false; 19 | } 20 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/DiagnosticHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace NetEscapades.EnumGenerators.Interceptors; 4 | 5 | public static class DiagnosticHelper 6 | { 7 | public static readonly DiagnosticDescriptor CsharpVersionLooLow = new( 8 | #pragma warning disable RS2008 // Enable Analyzer Release Tracking 9 | id: "NEEG001", 10 | #pragma warning restore RS2008 11 | title: "Language version too low", 12 | messageFormat: "Interceptors require at least C# version 11", 13 | category: "Requirements", 14 | defaultSeverity: DiagnosticSeverity.Warning, 15 | isEnabledByDefault: true); 16 | } -------------------------------------------------------------------------------- /samples/ToStringFastExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NetEscapades.EnumGenerators; 3 | 4 | var value = ExampleEnums.First; 5 | Console.WriteLine(value.ToStringFast()); 6 | 7 | var flags = FlagsEnum.Flag1 | FlagsEnum.Flag3; 8 | 9 | Console.WriteLine(flags.ToStringFast()); 10 | Console.WriteLine($"HasFlag(Flag1), {flags.HasFlagFast(FlagsEnum.Flag1)}"); 11 | Console.WriteLine($"HasFlag(Flag1), {flags.HasFlagFast(FlagsEnum.Flag2)}"); 12 | 13 | [EnumExtensions] 14 | internal enum ExampleEnums 15 | { 16 | First, 17 | Second, 18 | Third, 19 | } 20 | 21 | [EnumExtensions] 22 | [Flags] 23 | internal enum FlagsEnum 24 | { 25 | Flag1, 26 | Flag2, 27 | Flag3, 28 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/SerializationTransform.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | /// 4 | /// Transform to apply when calling ToStringFast 5 | /// 6 | public enum SerializationTransform 7 | { 8 | /// 9 | /// Don't apply a transform to the ToStringFast() result. 10 | /// 11 | None, 12 | 13 | /// 14 | /// Call on the ToStringFast() result. 15 | /// 16 | LowerInvariant, 17 | 18 | /// 19 | /// Call on the ToStringFast() result. 20 | /// 21 | UpperInvariant, 22 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | $(DefineConstants);NETESCAPADES_ENUMGENERATORS_SYSTEM_MEMORY 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Attributes.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | internal static class Attributes 4 | { 5 | public const string DisplayAttribute = "System.ComponentModel.DataAnnotations.DisplayAttribute"; 6 | public const string DescriptionAttribute = "System.ComponentModel.DescriptionAttribute"; 7 | public const string EnumMemberAttribute = "System.Runtime.Serialization.EnumMemberAttribute"; 8 | 9 | public const string EnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute"; 10 | public const string ExternalEnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute`1"; 11 | public const string FlagsAttribute = "System.FlagsAttribute"; 12 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests/InterceptionAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Foo; 4 | using NetEscapades.EnumGenerators; 5 | using NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 6 | 7 | [assembly:Interceptable] 8 | [assembly:Interceptable] 9 | [assembly:Interceptable] 10 | [assembly:Interceptable] 11 | [assembly:Interceptable] 12 | [assembly:Interceptable(ExtensionClassName="SomeExtension", ExtensionClassNamespace = "SomethingElse")] 13 | [assembly:Interceptable] 14 | [assembly:Interceptable] 15 | [assembly:Interceptable] 16 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/LocationInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace NetEscapades.EnumGenerators; 5 | 6 | internal record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) 7 | { 8 | public Location ToLocation() 9 | => Location.Create(FilePath, TextSpan, LineSpan); 10 | 11 | public static LocationInfo? CreateFrom(SyntaxNode node) 12 | => CreateFrom(node.GetLocation()); 13 | 14 | public static LocationInfo? CreateFrom(Location location) 15 | { 16 | if (location.SourceTree is null) 17 | { 18 | return null; 19 | } 20 | 21 | return new LocationInfo(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); 22 | } 23 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/LocationInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace NetEscapades.EnumGenerators.Interceptors; 5 | 6 | internal record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) 7 | { 8 | public Location ToLocation() 9 | => Location.Create(FilePath, TextSpan, LineSpan); 10 | 11 | public static LocationInfo? CreateFrom(SyntaxNode node) 12 | => CreateFrom(node.GetLocation()); 13 | 14 | public static LocationInfo? CreateFrom(Location location) 15 | { 16 | if (location.SourceTree is null) 17 | { 18 | return null; 19 | } 20 | 21 | return new LocationInfo(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.NetStandard/NetEscapades.EnumGenerators.Nuget.NetStandard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | enable 7 | $(DefineConstants);NUGET_NETSTANDARD_INTERCEPTOR_TESTS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory/NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | enable 7 | $(DefineConstants);NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests/InterceptionAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Foo; 4 | using NetEscapades.EnumGenerators; 5 | #if NETSTANDARD_INTERCEPTOR_TESTS 6 | using NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 7 | #elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS 8 | using NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests; 9 | #else 10 | #error Unknown integration tests 11 | #endif 12 | 13 | [assembly:Interceptable] 14 | [assembly:Interceptable] 15 | [assembly:Interceptable] 16 | [assembly:Interceptable] 17 | [assembly:Interceptable] 18 | [assembly:Interceptable(ExtensionClassName="SomeExtension", ExtensionClassNamespace = "SomethingElse")] 19 | [assembly:Interceptable] 20 | [assembly:Interceptable] 21 | [assembly:Interceptable] 22 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/TrackingNames.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators.Interceptors; 2 | 3 | /// 4 | /// Names that are attached to incremental generator stages for tracking 5 | /// 6 | public class TrackingNames 7 | { 8 | public const string InitialExtraction = nameof(InitialExtraction); 9 | public const string InitialExternalExtraction = nameof(InitialExternalExtraction); 10 | public const string InitialInterceptable = nameof(InitialInterceptable); 11 | public const string InitialInterceptableOnly = nameof(InitialInterceptableOnly); 12 | public const string RemovingNulls = nameof(RemovingNulls); 13 | public const string InterceptedLocations = nameof(InterceptedLocations); 14 | public const string Settings = nameof(Settings); 15 | public const string EnumInterceptions = nameof(EnumInterceptions); 16 | public const string ExternalInterceptions = nameof(ExternalInterceptions); 17 | public const string AdditionalInterceptions = nameof(AdditionalInterceptions); 18 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/EnumToIntercept.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp; 2 | 3 | namespace NetEscapades.EnumGenerators.Interceptors; 4 | 5 | public readonly record struct EnumToIntercept(string Name, string FullyQualifiedName, string Namespace); 6 | 7 | #pragma warning disable RSEXPERIMENTAL002 // / Experimental interceptable location API 8 | public record CandidateInvocation(InterceptableLocation Location, string EnumName, InterceptorTarget Target); 9 | #pragma warning restore RSEXPERIMENTAL002 10 | 11 | public record MethodToIntercept( 12 | EquatableArray Invocations, 13 | string ExtensionTypeName, 14 | string FullyQualifiedName, 15 | string EnumNamespace) 16 | { 17 | public MethodToIntercept(EquatableArray invocations, EnumToIntercept enumToIntercept) 18 | : this(invocations, enumToIntercept.Name, enumToIntercept.FullyQualifiedName, enumToIntercept.Namespace) 19 | { 20 | } 21 | } 22 | 23 | public enum InterceptorTarget 24 | { 25 | ToString, 26 | HasFlag, 27 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard/NetEscapades.EnumGenerators.NetStandard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | enable 7 | $(DefineConstants);NETSTANDARD_INTEGRATION_TESTS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 andrewlock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Benchmarks/NetEscapades.EnumGenerators.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net48;net6.0;net10.0 6 | enable 7 | enable 8 | false 9 | latest 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumParseOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace NetEscapades.EnumGenerators.Benchmarks; 6 | 7 | public class EnumParseOptionsTests 8 | { 9 | [Fact] 10 | public void Default() 11 | { 12 | EnumParseOptions options = default; 13 | options.AllowMatchingMetadataAttribute.Should().BeFalse(); 14 | options.EnableNumberParsing.Should().BeTrue(); 15 | options.ComparisonType.Should().Be(StringComparison.Ordinal); 16 | } 17 | 18 | [Fact] 19 | public void DefaultConstructor() 20 | { 21 | var options = new EnumParseOptions(); 22 | options.AllowMatchingMetadataAttribute.Should().BeFalse(); 23 | options.EnableNumberParsing.Should().BeTrue(); 24 | options.ComparisonType.Should().Be(StringComparison.Ordinal); 25 | } 26 | 27 | [Fact] 28 | public void SingleValue() 29 | { 30 | var options = new EnumParseOptions(comparisonType: StringComparison.OrdinalIgnoreCase); 31 | options.AllowMatchingMetadataAttribute.Should().BeFalse(); 32 | options.EnableNumberParsing.Should().BeTrue(); 33 | options.ComparisonType.Should().Be(StringComparison.OrdinalIgnoreCase); 34 | } 35 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/EnumToGenerate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace NetEscapades.EnumGenerators; 5 | 6 | public readonly record struct EnumToGenerate 7 | { 8 | public readonly string Name; 9 | public readonly string FullyQualifiedName; 10 | public readonly string Namespace; 11 | public readonly bool IsPublic; 12 | public readonly bool HasFlags; 13 | public readonly string UnderlyingType; 14 | public readonly MetadataSource? MetadataSource; 15 | 16 | /// 17 | /// Key is the enum name. 18 | /// 19 | public readonly EquatableArray<(string Key, EnumValueOption Value)> Names; 20 | 21 | public EnumToGenerate( 22 | string name, 23 | string ns, 24 | string fullyQualifiedName, 25 | string underlyingType, 26 | bool isPublic, 27 | List<(string Key, EnumValueOption Value)> names, 28 | bool hasFlags, 29 | MetadataSource? metadataSource) 30 | { 31 | Name = name; 32 | Namespace = ns; 33 | UnderlyingType = underlyingType; 34 | Names = new EquatableArray<(string, EnumValueOption)>(names.ToArray()); 35 | HasFlags = hasFlags; 36 | IsPublic = isPublic; 37 | FullyQualifiedName = fullyQualifiedName; 38 | MetadataSource = metadataSource; 39 | } 40 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard.SystemMemory/NetEscapades.EnumGenerators.NetStandard.SystemMemory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | netstandard2.0 7 | false 8 | enable 9 | $(DefineConstants);NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Andrew Lock 7 | Copyright © AndrewLock 8 | en-GB 9 | false 10 | MIT 11 | https://github.com/andrewlock/NetEscapades.EnumGenerators 12 | enums attribute generator generation codegen codegenerator codegeneration netescapades 13 | false 14 | true 15 | true 16 | icon.png 17 | 18 | 19 | 20 | true 21 | latest 22 | False 23 | $(MSBuildThisFileDirectory)\artifacts 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors.Attributes/InterceptableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators 2 | { 3 | /// 4 | /// Add to an assembly to indicate that usages of the enum should 5 | /// be automatically intercepted to use the extension methods 6 | /// generated by EnumExtensionsAttribute in this project. 7 | /// Note that the extension methods must be accessible from this project, 8 | /// otherwise you will receive compilation errors 9 | /// 10 | [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] 11 | [System.Diagnostics.Conditional("NETESCAPADES_ENUMGENERATORS_USAGES")] 12 | public class InterceptableAttribute : System.Attribute 13 | where T: System.Enum 14 | { 15 | /// 16 | /// The namespace generated for the extension class. If not provided, 17 | /// and the referenced enum is in a different project, the namespace 18 | /// of the extension methods are assumed to be the same as the enum. 19 | /// 20 | public string? ExtensionClassNamespace { get; set; } 21 | 22 | /// 23 | /// The name used for the extension class. If not provided, 24 | /// and the referenced enum is in a different project, the enum name 25 | /// with an Extensions suffix will be assumed. For example for 26 | /// an Enum called StatusCodes, the assumed name will be StatusCodesExtensions. 27 | /// 28 | public string? ExtensionClassName { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Tests/NetEscapades.EnumGenerators.Tests.Project.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NetEscapades.EnumGenerators.Tests 5 | $(AssemblyName) 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/EnumValueOption.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | public readonly struct EnumValueOption : IEquatable 4 | { 5 | /// 6 | /// Custom name set by the [Display(Name)] attribute. 7 | /// 8 | private readonly string? _displayName; 9 | private readonly string? _description; 10 | private readonly string? _enumMemberValue; 11 | public object ConstantValue { get; } 12 | 13 | public EnumValueOption(string? displayName, string? description, string? enumMemberValue, object constantValue) 14 | { 15 | _displayName = displayName; 16 | ConstantValue = constantValue; 17 | _description = description; 18 | _enumMemberValue = enumMemberValue; 19 | } 20 | 21 | public bool Equals(EnumValueOption other) 22 | { 23 | return _displayName == other._displayName && 24 | _description == other._description && 25 | _enumMemberValue == other._enumMemberValue && 26 | Equals(ConstantValue, other.ConstantValue); 27 | } 28 | 29 | public string? GetMetadataName(MetadataSource metadataSource) 30 | => metadataSource switch 31 | { 32 | MetadataSource.DisplayAttribute => _displayName, 33 | MetadataSource.DescriptionAttribute => _description, 34 | MetadataSource.EnumMemberAttribute => _enumMemberValue, 35 | _ => null, 36 | }; 37 | 38 | public static EnumValueOption CreateWithoutAttributes(object constantValue) 39 | => new(null, null, null, constantValue); 40 | } 41 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | $(MSBuildThisFileDirectory)../artifacts 7 | true 8 | NU1901;NU1902;NU1903;NU1904 9 | net6.0;net7.0;net8.0;net9.0;net10.0 10 | net48;netcoreapp2.1;netcoreapp3.1;$(TargetFrameworks) 11 | 12 | 13 | en 14 | latest 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/SerializationOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | 7 | public class SerializationOptionsTests 8 | { 9 | [Fact] 10 | public void Default() 11 | { 12 | SerializationOptions options = default; 13 | options.UseMetadataAttributes.Should().BeFalse(); 14 | options.Transform.Should().Be(SerializationTransform.None); 15 | } 16 | 17 | [Fact] 18 | public void DefaultConstructor() 19 | { 20 | var options = new SerializationOptions(); 21 | options.UseMetadataAttributes.Should().BeFalse(); 22 | options.Transform.Should().Be(SerializationTransform.None); 23 | } 24 | 25 | [Fact] 26 | public void Transform() 27 | { 28 | var options = new SerializationOptions(transform: SerializationTransform.UpperInvariant); 29 | options.UseMetadataAttributes.Should().BeFalse(); 30 | options.Transform.Should().Be(SerializationTransform.UpperInvariant); 31 | } 32 | 33 | [Fact] 34 | public void Metadata() 35 | { 36 | var options = new SerializationOptions(true); 37 | options.UseMetadataAttributes.Should().BeTrue(); 38 | options.Transform.Should().Be(SerializationTransform.None); 39 | } 40 | 41 | [Fact] 42 | public void TransformAndMetadata() 43 | { 44 | var options = new SerializationOptions(true, transform: SerializationTransform.UpperInvariant); 45 | options.UseMetadataAttributes.Should().BeTrue(); 46 | options.Transform.Should().Be(SerializationTransform.UpperInvariant); 47 | } 48 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/SerializationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators; 2 | 3 | /// 4 | /// Options to apply when calling ToStringFast on an enum. 5 | /// 6 | public readonly struct SerializationOptions 7 | { 8 | /// 9 | /// Create an instance of 10 | /// 11 | /// Sets whether the value of any metadata value attribute 12 | /// values applied to an enum should be used in the ToStringFast call. 13 | /// Sets the to use when serializing the enum value. 14 | public SerializationOptions( 15 | bool useMetadataAttributes = false, 16 | SerializationTransform transform = SerializationTransform.None) 17 | { 18 | UseMetadataAttributes = useMetadataAttributes; 19 | Transform = transform; 20 | } 21 | 22 | /// 23 | /// Gets whether the value of any metadata attributes applied to an enum member 24 | /// should be used as the ToString() value for the enum. 25 | /// 26 | /// 27 | /// By default, it's set to , so the value of metadata attributes on the 28 | /// enum values are ignored. 29 | /// 30 | public bool UseMetadataAttributes { get; } 31 | 32 | /// 33 | /// Gets the to use during parsing. 34 | /// 35 | /// 36 | /// By default, it's set to , and the value is not transformed. 37 | /// 38 | public SerializationTransform Transform { get; } 39 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard.IntegrationTests/NetEscapades.EnumGenerators.NetStandard.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NETSTANDARD_INTEGRATION_TESTS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | all 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests/NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NUGET_INTERCEPTOR_TESTS 7 | $(DefineConstants);READONLYSPAN 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.IntegrationTests/NetEscapades.EnumGenerators.Nuget.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NUGET_INTEGRATION_TESTS 7 | $(DefineConstants);READONLYSPAN 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | all 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests/NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS;READONLYSPAN 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | all 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/MetadataSource.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators 2 | { 3 | /// 4 | /// Defines where to obtain metadata for serializing and deserializing the enum 5 | /// 6 | public enum MetadataSource 7 | { 8 | /// 9 | /// Don't use attributes applied to enum members as a source of metadata for 10 | /// ToStringFast() and TryParse(). The name of the enum member 11 | /// will always be used for serialization. 12 | /// 13 | None, 14 | 15 | /// 16 | /// Use values provided in System.ComponentModel.DataAnnotations.DisplayAttribute for 17 | /// determining the value to use for ToStringFast() and TryParse(). 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 | /// 21 | DisplayAttribute, 22 | 23 | /// 24 | /// Use values provided in for 25 | /// determining the value to use for ToStringFast() and TryParse(). 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 | /// 29 | DescriptionAttribute, 30 | 31 | /// 32 | /// Use values provided in for 33 | /// determining the value to use for ToStringFast() and TryParse(). 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 | /// 37 | EnumMemberAttribute, 38 | } 39 | } -------------------------------------------------------------------------------- /samples/ToStringFastExample/ToStringFastExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 6 | net48;$(TargetFrameworks) 7 | enable 8 | true 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.IntegrationTests/NetEscapades.EnumGenerators.Nuget.IntegrationTests.Project.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 5 | net48;$(TargetFrameworks) 6 | false 7 | enable 8 | $(DefineConstants);READONLYSPAN 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | all 31 | 32 | 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | all 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.IntegrationTests/NetEscapades.EnumGenerators.Nuget.NetStandard.SystemMemory.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NUGET_SYSTEMMEMORY_INTEGRATION_TESTS;READONLYSPAN 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | all 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests/NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NUGET_NETSTANDARD_INTERCEPTOR_TESTS 7 | $(DefineConstants);READONLYSPAN 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | all 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Tests/EquatableArrayTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace NetEscapades.EnumGenerators.Tests; 5 | 6 | public class EquatableArrayTests 7 | { 8 | [Fact] 9 | public void PrimitiveComparison() 10 | { 11 | int[] val1 = [1, 2, 3, 4, 5]; 12 | int[] val2 = [1, 2, 3, 4, 5]; 13 | 14 | var arr1 = new EquatableArray(val1); 15 | var arr2 = new EquatableArray(val2); 16 | 17 | arr1.Equals(arr2).Should().BeTrue(); 18 | } 19 | 20 | [Fact] 21 | public void RecordComparison() 22 | { 23 | Record[] val1 = [new(1), new(2), new(3), new(4), new(5)]; 24 | Record[] val2 = [new(1), new(2), new(3), new(4), new(5)]; 25 | 26 | var arr1 = new EquatableArray(val1); 27 | var arr2 = new EquatableArray(val2); 28 | 29 | arr1.Equals(arr2).Should().BeTrue(); 30 | } 31 | 32 | [Fact] 33 | public void NestedEquatableArrayComparison() 34 | { 35 | EquatableArray[] val1 = [new([1]), new([2]), new([3]), new([4]), new([5])]; 36 | EquatableArray[] val2 = [new([1]), new([2]), new([3]), new([4]), new([5])]; 37 | 38 | var arr1 = new EquatableArray>(val1); 39 | var arr2 = new EquatableArray>(val2); 40 | 41 | arr1.Equals(arr2).Should().BeTrue(); 42 | } 43 | 44 | [Fact] 45 | public void BoxedNestedEquatableArrayComparison() 46 | { 47 | EquatableArray[] val1 = [new([1]), new([2]), new([3]), new([4]), new([5])]; 48 | EquatableArray[] val2 = [new([1]), new([2]), new([3]), new([4]), new([5])]; 49 | 50 | object arr1 = new EquatableArray>(val1); 51 | var arr2 = new EquatableArray>(val2); 52 | 53 | arr1.Equals(arr2).Should().BeTrue(); 54 | arr2.Equals(arr1).Should().BeTrue(); 55 | } 56 | 57 | public record Record 58 | { 59 | public Record(int value) 60 | { 61 | Value = value; 62 | } 63 | 64 | public int Value { get; } 65 | } 66 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/AnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | using NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | using NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | using NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | using NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | using NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | using NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS 17 | using NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests; 18 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 19 | using NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 20 | #else 21 | #error Unknown integration tests 22 | #endif 23 | 24 | namespace NetEscapades.EnumGenerators.Benchmarks; 25 | 26 | public class AnalyzerTests 27 | { 28 | // These method calls should all be flagged by the analyzer 29 | [Fact] 30 | public void Neeg004Testing() 31 | { 32 | #pragma warning disable NEEG004 33 | var test = EnumInSystem.First; 34 | _ = test.ToString(); 35 | _ = EnumInSystem.First.ToString(); 36 | _ = EnumInSystem.First.ToString("G"); 37 | _ = EnumInSystem.First.ToString("x"); // no error 38 | _ = EnumInSystem.First.ToString(format: "g"); 39 | _ = EnumInSystem.First.ToString(format: null); // no error 40 | _ = DateTimeKind.Local.ToString(); 41 | _ = $"Some value: {test} <-"; 42 | _ = $"Some value: {test:G} <-"; 43 | _ = $"Some value: {EnumInSystem.First} <-"; 44 | _ = $"Some value: {EnumInSystem.First:G} <-"; 45 | #pragma warning restore NEEG004 46 | } 47 | 48 | [Fact] 49 | public void Neeg005Testing() 50 | { 51 | #pragma warning disable NEEG005 52 | var test = FlagsEnum.First; 53 | _ = test.HasFlag(FlagsEnum.Second); 54 | _ = $"Some value: {test.HasFlag(FlagsEnum.Second)} <-"; 55 | #pragma warning restore NEEG005 56 | } 57 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/NetEscapades.EnumGenerators.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | $(DefineConstants);INTEGRATION_TESTS 6 | $(DefineConstants);READONLYSPAN; 7 | true 8 | EnumMemberAttribute 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | all 34 | 35 | 36 | runtime; build; native; contentfiles; analyzers; buildtransitive 37 | all 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Interceptors.IntegrationTests/Enums.cs: -------------------------------------------------------------------------------- 1 | using Foo; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.IO; 5 | using NetEscapades.EnumGenerators; 6 | using Xunit; 7 | 8 | [assembly:NetEscapades.EnumGenerators.EnumExtensions()] 9 | [assembly:NetEscapades.EnumGenerators.EnumExtensions()] 10 | 11 | namespace Foo 12 | { 13 | // causes Error CS0426 : The type name 'TestEnum' does not exist in the type 'Foo'. 14 | // workaround is to use global prefix 15 | public class Foo { } 16 | 17 | [EnumExtensions] 18 | public enum EnumInFoo 19 | { 20 | First = 0, 21 | [Display(Name = "2nd")] 22 | Second = 1, 23 | Third = 2, 24 | } 25 | } 26 | 27 | #if NUGET_INTERCEPTOR_TESTS 28 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests 29 | #elif INTERCEPTOR_TESTS 30 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests 31 | #else 32 | #error Unknown project combination 33 | #endif 34 | { 35 | [EnumExtensions] 36 | public enum EnumWithDisplayNameInNamespace 37 | { 38 | First = 0, 39 | 40 | [System.ComponentModel.Description("2nd")] Second = 1, 41 | 42 | Third = 2, 43 | } 44 | 45 | [EnumExtensions(ExtensionClassName="SomeExtension", ExtensionClassNamespace = "SomethingElse")] 46 | public enum EnumWithExtensionInOtherNamespace 47 | { 48 | First = 0, 49 | 50 | [Display(Name = "2nd")] Second = 1, 51 | 52 | Third = 2, 53 | } 54 | 55 | [EnumExtensions] 56 | [Flags] 57 | public enum FlagsEnum 58 | { 59 | None = 0, 60 | First = 1 << 0, 61 | Second = 1 << 1, 62 | Third = 1 << 2, 63 | Fourth = 1 << 3, 64 | ThirdAndFourth = Third | Fourth, 65 | } 66 | 67 | [EnumExtensions] 68 | public enum StringTesting 69 | { 70 | [System.ComponentModel.Description("Quotes \"")] 71 | Quotes, 72 | 73 | [System.ComponentModel.Description(@"Literal Quotes """)] 74 | LiteralQuotes, 75 | 76 | [System.ComponentModel.Description("Backslash \\")] 77 | Backslash, 78 | 79 | [System.ComponentModel.Description(@"LiteralBackslash \")] 80 | BackslashLiteral, 81 | 82 | [System.ComponentModel.Description("Line\nBreak")] 83 | LineBreak, 84 | } 85 | 86 | [EnumExtensions(IsInterceptable = false)] 87 | public enum NonInterceptableEnum 88 | { 89 | First = 0, 90 | [Display(Name = "2nd")] Second = 1, 91 | Third = 2, 92 | } 93 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Benchmarks/EnumHelper.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Reflection; 3 | 4 | namespace NetEscapades.EnumGenerators.Benchmarks; 5 | 6 | internal static class EnumHelper where T : struct 7 | { 8 | internal static bool TryParseByDisplayName(string name, bool ignoreCase, out T enumValue) 9 | { 10 | enumValue = default; 11 | 12 | var stringComparisonOption = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; 13 | var enumValues = (T[])Enum.GetValues(typeof(T)); 14 | foreach (var value in enumValues) 15 | { 16 | if (TryGetDisplayName(value.ToString(), out var displayName) && displayName!.Equals(name, stringComparisonOption)) 17 | { 18 | enumValue = value; 19 | return true; 20 | } 21 | } 22 | 23 | return false; 24 | } 25 | 26 | private static bool TryGetDisplayName( 27 | string? value, 28 | #if NETCOREAPP3_0_OR_GREATER 29 | [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? displayName) 30 | #else 31 | out string? displayName) 32 | #endif 33 | { 34 | displayName = default; 35 | 36 | if (typeof(T).IsEnum) 37 | { 38 | // Prevent: Warning CS8604 Possible null reference argument for parameter 'name' in 'MemberInfo[] Type.GetMember(string name)' 39 | if (value is not null) 40 | { 41 | var memberInfo = typeof(T).GetMember(value); 42 | if (memberInfo.Length > 0) 43 | { 44 | displayName = memberInfo[0].GetCustomAttribute()?.GetName(); 45 | if (displayName is null) 46 | { 47 | return false; 48 | } 49 | 50 | return true; 51 | } 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | 58 | internal static string GetDisplayName(T value) 59 | { 60 | if (typeof(T).IsEnum) 61 | { 62 | var memberInfo = typeof(T).GetMember(value.ToString()!); 63 | if (memberInfo.Length > 0) 64 | { 65 | var displayName = memberInfo[0].GetCustomAttribute()!.GetName(); 66 | if (displayName is null) 67 | { 68 | return string.Empty; 69 | } 70 | 71 | return displayName; 72 | } 73 | } 74 | 75 | return string.Empty; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/NetEscapades.EnumGenerators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | NetEscapades.EnumGenerators 7 | $(PackageId) 8 | $(PackageId) 9 | enable 10 | true 11 | A source generator for creating helper extension methods on enums using a [EnumExtensions] attribute 12 | README.md 13 | true 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | *.sh eol=lf 6 | 7 | ############################################################################### 8 | # Set default behavior for command prompt diff. 9 | # 10 | # This is need for earlier builds of msysgit that does not have it on by 11 | # default for csharp files. 12 | # Note: This is only used by command line 13 | ############################################################################### 14 | #*.cs diff=csharp 15 | 16 | ############################################################################### 17 | # Set the merge driver for project and solution files 18 | # 19 | # Merging from the command prompt will add diff markers to the files if there 20 | # are conflicts (Merging from VS is not affected by the settings below, in VS 21 | # the diff markers are never inserted). Diff markers may cause the following 22 | # file extensions to fail to load in VS. An alternative would be to treat 23 | # these files as binary and thus will always conflict and require user 24 | # intervention with every merge. To do so, just uncomment the entries below 25 | ############################################################################### 26 | #*.sln merge=binary 27 | #*.csproj merge=binary 28 | #*.vbproj merge=binary 29 | #*.vcxproj merge=binary 30 | #*.vcproj merge=binary 31 | #*.dbproj merge=binary 32 | #*.fsproj merge=binary 33 | #*.lsproj merge=binary 34 | #*.wixproj merge=binary 35 | #*.modelproj merge=binary 36 | #*.sqlproj merge=binary 37 | #*.wwaproj merge=binary 38 | 39 | ############################################################################### 40 | # behavior for image files 41 | # 42 | # image files are treated as binary by default. 43 | ############################################################################### 44 | #*.jpg binary 45 | #*.png binary 46 | #*.gif binary 47 | 48 | ############################################################################### 49 | # diff behavior for common document formats 50 | # 51 | # Convert binary document formats to text before diffing them. This feature 52 | # is only available from the command line. Turn it on by uncommenting the 53 | # entries below. 54 | ############################################################################### 55 | #*.doc diff=astextplain 56 | #*.DOC diff=astextplain 57 | #*.docx diff=astextplain 58 | #*.DOCX diff=astextplain 59 | #*.dot diff=astextplain 60 | #*.DOT diff=astextplain 61 | #*.pdf diff=astextplain 62 | #*.PDF diff=astextplain 63 | #*.rtf diff=astextplain 64 | #*.RTF diff=astextplain 65 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Tests/NetEscapades.EnumGenerators.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NetEscapades.EnumGenerators.Tests 5 | $(AssemblyName) 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | EnumExtensionsAttribute.cs 41 | 42 | 43 | MetadataSource.cs 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Interceptors.IntegrationTests/NetEscapades.EnumGenerators.Interceptors.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | $(DefineConstants);INTERCEPTOR_TESTS 6 | $(DefineConstants);READONLYSPAN 7 | true 8 | true 9 | $(InterceptorsPreviewNamespaces);NetEscapades.EnumGenerators 10 | $(InterceptorsNamespaces);NetEscapades.EnumGenerators 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | all 41 | 42 | 43 | runtime; build; native; contentfiles; analyzers; buildtransitive 44 | all 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests/NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | enable 6 | $(DefineConstants);NETSTANDARD_INTERCEPTOR_TESTS 7 | true 8 | $(InterceptorsPreviewNamespaces);NetEscapades.EnumGenerators 9 | $(InterceptorsNamespaces);NetEscapades.EnumGenerators 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | runtime; build; native; contentfiles; analyzers; buildtransitive 45 | all 46 | 47 | 48 | runtime; build; native; contentfiles; analyzers; buildtransitive 49 | all 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/EnumParseOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NetEscapades.EnumGenerators; 4 | 5 | /// 6 | /// Defines the options use when parsing enums using members provided by NetEscapades.EnumGenerator. 7 | /// 8 | public readonly struct EnumParseOptions 9 | { 10 | private const StringComparison DefaultComparisonType = StringComparison.Ordinal; 11 | 12 | private readonly StringComparison? _comparisonType; 13 | private readonly bool _blockNumberParsing; 14 | 15 | /// 16 | /// Create an instance of 17 | /// 18 | /// Sets the to use during parsing. 19 | /// Sets whether the value of the selected metadata value attribute 20 | /// values applied to an enum should be used as the parse value for an enum. 21 | /// Sets a value defining whether numbers should be parsed as a fallback when 22 | /// other parsing fails. 23 | public EnumParseOptions( 24 | StringComparison comparisonType = DefaultComparisonType, 25 | bool allowMatchingMetadataAttribute = false, 26 | bool enableNumberParsing = true) 27 | { 28 | _comparisonType = comparisonType; 29 | AllowMatchingMetadataAttribute = allowMatchingMetadataAttribute; 30 | _blockNumberParsing = !enableNumberParsing; 31 | } 32 | 33 | /// 34 | /// Gets or sets the to use during parsing. 35 | /// 36 | /// 37 | /// By default, it's set to , and a case-sensitive 38 | /// comparison will be used. 39 | /// 40 | public StringComparison ComparisonType => _comparisonType ?? DefaultComparisonType; 41 | 42 | /// 43 | /// Gets or sets whether the value of the selected metadata value attribute 44 | /// values applied to an enum should be used as the parse value for an enum. 45 | /// 46 | /// 47 | /// By default, it's set to , so the value of any metadata attributes on the 48 | /// enum values are ignored. Set to to enable parsing using the applicable 49 | /// for each enum member. 50 | /// 51 | public bool AllowMatchingMetadataAttribute { get; } 52 | 53 | /// 54 | /// Gets or sets a value defining whether numbers should be parsed as a fallback when 55 | /// other parsing fails. 56 | /// 57 | /// 58 | /// By default, it's set to , and numbers will be parsed as well as names. 59 | /// 60 | public bool EnableNumberParsing => !_blockNumberParsing; 61 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/NetEscapades.EnumGenerators.Interceptors.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | NetEscapades.EnumGenerators.Interceptors 7 | $(PackageId) 8 | $(PackageId) 9 | enable 10 | true 11 | A source generator interceptor for automatically intercepting calls to ToString() on enums, and replacing them with calls to ToStringFast() generated from NetEscapades.EnumGenerators 12 | README.md 13 | true 14 | true 15 | $(PackageTags) interceptor 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/HasFlagCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeActions; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | 9 | namespace NetEscapades.EnumGenerators.Diagnostics; 10 | 11 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(HasFlagCodeFixProvider)), Shared] 12 | public class HasFlagCodeFixProvider : CodeFixProvider 13 | { 14 | private const string Title = "Replace with HasFlagFast()"; 15 | 16 | public sealed override ImmutableArray FixableDiagnosticIds 17 | => ImmutableArray.Create(HasFlagAnalyzer.DiagnosticId); 18 | 19 | public sealed override FixAllProvider GetFixAllProvider() 20 | => WellKnownFixAllProviders.BatchFixer; 21 | 22 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 23 | { 24 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 25 | if (root is null) 26 | { 27 | return; 28 | } 29 | 30 | var diagnostic = context.Diagnostics.First(); 31 | var diagnosticSpan = diagnostic.Location.SourceSpan; 32 | 33 | // Find the node at the diagnostic location 34 | var node = root.FindNode(diagnosticSpan); 35 | 36 | // Check if this is a HasFlag invocation 37 | if (node is IdentifierNameSyntax identifierName) 38 | { 39 | // Register a code action for HasFlag() replacement 40 | context.RegisterCodeFix( 41 | CodeAction.Create( 42 | title: Title, 43 | createChangedDocument: c => ReplaceHasFlagWithHasFlagFast(context.Document, identifierName, c), 44 | equivalenceKey: Title), 45 | context.Diagnostics); 46 | } 47 | } 48 | 49 | private static async Task ReplaceHasFlagWithHasFlagFast( 50 | Document document, 51 | IdentifierNameSyntax identifierName, 52 | CancellationToken cancellationToken) 53 | { 54 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 55 | if (root is null) 56 | { 57 | return document; 58 | } 59 | 60 | // Create the new identifier with "HasFlagFast" 61 | var newIdentifier = SyntaxFactory.IdentifierName("HasFlagFast") 62 | .WithTriviaFrom(identifierName); 63 | 64 | // Create new member access with the new identifier 65 | var memberAccess = identifierName.Parent as MemberAccessExpressionSyntax; 66 | if (memberAccess is null) 67 | { 68 | return document; 69 | } 70 | 71 | var newMemberAccess = memberAccess.WithName(newIdentifier); 72 | 73 | // Replace the old member access with the new one 74 | var newRoot = root.ReplaceNode(memberAccess, newMemberAccess); 75 | 76 | return document.WithSyntaxRoot(newRoot); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/BuildAndPack.yml: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # 3 | # 4 | # This code was generated. 5 | # 6 | # - To turn off auto-generation set: 7 | # 8 | # [GitHubActions (AutoGenerate = false)] 9 | # 10 | # - To trigger manual generation invoke: 11 | # 12 | # nuke --generate-configuration GitHubActions_BuildAndPack --host GitHubActions 13 | # 14 | # 15 | # ------------------------------------------------------------------------------ 16 | 17 | name: BuildAndPack 18 | 19 | on: 20 | push: 21 | branches: 22 | - master 23 | - main 24 | tags: 25 | - '*' 26 | pull_request: 27 | branches: 28 | - '*' 29 | 30 | jobs: 31 | build-and-test: 32 | strategy: 33 | matrix: 34 | include: 35 | - os: windows 36 | vm: windows-latest 37 | - os: linux 38 | vm: ubuntu-latest 39 | - os: macos 40 | vm: macos-15 41 | env: 42 | MSBuildEnableWorkloadResolver: false 43 | DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: "true" 44 | name: ${{ matrix.os}} 45 | runs-on: ${{ matrix.vm}} 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-dotnet@v4 49 | with: 50 | dotnet-version: | 51 | 10.0.x 52 | 9.0.x 53 | 8.0.x 54 | 7.0.x 55 | 6.0.x 56 | 3.1.x 57 | 2.1.x 58 | 59 | # - run: dotnet new globaljson --sdk-version "8.0.402" --force 60 | - name: Cache .nuke/temp, ~/.nuget/packages 61 | uses: actions/cache@v3 62 | with: 63 | path: | 64 | .nuke/temp 65 | ~/.nuget/packages 66 | !~/.nuget/packages/netescapades.enumgenerators 67 | !~/.nuget/packages/netescapades.enumgenerators.interceptors 68 | key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj') }} 69 | 70 | - name: Run './build.cmd Clean Test TestPackage PushToNuGet 71 | run: ./build.cmd Clean Test TestPackage PushToNuGet 72 | env: 73 | NuGetToken: ${{ secrets.NUGET_TOKEN || 'NOT_SET'}} 74 | 75 | - uses: actions/upload-artifact@v4 76 | with: 77 | name: packages-${{ matrix.os}} 78 | path: artifacts/packages 79 | - uses: actions/upload-artifact@v4 80 | with: 81 | name: results-${{ matrix.os}} 82 | path: artifacts/results 83 | 84 | publish-test-results: 85 | name: "Publish Tests Results" 86 | needs: build-and-test 87 | runs-on: ubuntu-latest 88 | permissions: 89 | checks: write 90 | pull-requests: write # needed unless run with comment_mode: off 91 | # contents: read # only needed for private repository 92 | # issues: read # only needed for private repository 93 | if: always() 94 | 95 | steps: 96 | - name: Download Artifacts 97 | uses: actions/download-artifact@v4 98 | with: 99 | path: artifacts/results 100 | 101 | - name: Publish Test Results 102 | uses: EnricoMi/publish-unit-test-result-action@v2 103 | with: 104 | files: "artifacts/**/*.trx" 105 | json_thousands_separator: "," 106 | check_run_annotations_branch: "*" 107 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/EnumInGenericTypeAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | namespace NetEscapades.EnumGenerators.Diagnostics; 8 | 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public class EnumInGenericTypeAnalyzer : DiagnosticAnalyzer 11 | { 12 | public const string DiagnosticId = "NEEG002"; 13 | public static readonly DiagnosticDescriptor Rule = new( 14 | #pragma warning disable RS2008 // Enable Analyzer Release Tracking 15 | id: DiagnosticId, 16 | #pragma warning restore RS2008 17 | title: "Enum in generic type not supported", 18 | messageFormat: "The enum '{0}' is nested inside a generic type. [EnumExtension] attribute is not supported.", 19 | category: "Usage", 20 | defaultSeverity: DiagnosticSeverity.Warning, 21 | isEnabledByDefault: true); 22 | 23 | public override ImmutableArray SupportedDiagnostics 24 | => ImmutableArray.Create(Rule); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 29 | context.EnableConcurrentExecution(); 30 | context.RegisterSyntaxNodeAction(AnalyzeEnumDeclaration, SyntaxKind.EnumDeclaration); 31 | } 32 | 33 | private static void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context) 34 | { 35 | var enumDeclaration = (EnumDeclarationSyntax)context.Node; 36 | 37 | // Check if enum has [EnumExtensions] attribute and capture its location 38 | AttributeSyntax? enumExtensionsAttribute = null; 39 | foreach (var attributeList in enumDeclaration.AttributeLists) 40 | { 41 | foreach (var attribute in attributeList.Attributes) 42 | { 43 | // Check attribute name syntactically first 44 | var attributeName = attribute.Name.ToString(); 45 | if (attributeName == "EnumExtensions" || attributeName == "EnumExtensionsAttribute") 46 | { 47 | // Verify with semantic model if needed for precision 48 | var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute); 49 | if (symbolInfo.Symbol is IMethodSymbol method && 50 | method.ContainingType.ToDisplayString() == Attributes.EnumExtensionsAttribute) 51 | { 52 | enumExtensionsAttribute = attribute; 53 | break; 54 | } 55 | } 56 | } 57 | 58 | if (enumExtensionsAttribute is not null) 59 | { 60 | break; 61 | } 62 | } 63 | 64 | if (enumExtensionsAttribute is null) 65 | { 66 | return; 67 | } 68 | 69 | // Get the enum symbol 70 | var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclaration); 71 | if (enumSymbol is null) 72 | { 73 | return; 74 | } 75 | 76 | // Check if nested in generic type 77 | if (SymbolHelpers.IsNestedInGenericType(enumSymbol)) 78 | { 79 | var diagnostic = Diagnostic.Create( 80 | Rule, 81 | enumExtensionsAttribute.GetLocation(), 82 | enumSymbol.Name); 83 | 84 | context.ReportDiagnostic(diagnostic); 85 | } 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/EquatableArray.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Immutable; 3 | 4 | namespace NetEscapades.EnumGenerators; 5 | 6 | /// 7 | /// An immutable, equatable array. This is equivalent to but with value equality support. 8 | /// 9 | /// The type of values in the array. 10 | public readonly struct EquatableArray : IEquatable>, IReadOnlyCollection 11 | where T : IEquatable 12 | { 13 | /// 14 | /// The underlying array. 15 | /// 16 | private readonly T[]? _array; 17 | 18 | /// 19 | /// Creates a new instance. 20 | /// 21 | /// The input to wrap. 22 | public EquatableArray(T[] array) 23 | { 24 | _array = array; 25 | } 26 | 27 | /// 28 | public bool Equals(EquatableArray array) 29 | { 30 | return AsSpan().SequenceEqual(array.AsSpan()); 31 | } 32 | 33 | /// 34 | public override bool Equals(object? obj) 35 | { 36 | return obj is EquatableArray array && this.Equals(array); 37 | } 38 | 39 | /// 40 | public override int GetHashCode() 41 | { 42 | if (_array is not T[] array) 43 | { 44 | return 0; 45 | } 46 | 47 | HashCode hashCode = default; 48 | 49 | foreach (T item in array) 50 | { 51 | hashCode.Add(item); 52 | } 53 | 54 | return hashCode.ToHashCode(); 55 | } 56 | 57 | /// 58 | /// Returns a wrapping the current items. 59 | /// 60 | /// A wrapping the current items. 61 | public ReadOnlySpan AsSpan() 62 | { 63 | return _array.AsSpan(); 64 | } 65 | 66 | /// 67 | IEnumerator IEnumerable.GetEnumerator() 68 | { 69 | return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); 70 | } 71 | 72 | /// 73 | IEnumerator IEnumerable.GetEnumerator() 74 | { 75 | return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); 76 | } 77 | 78 | public int Count => _array?.Length ?? 0; 79 | 80 | /// 81 | /// Checks whether two values are the same. 82 | /// 83 | /// The first value. 84 | /// The second value. 85 | /// Whether and are equal. 86 | public static bool operator ==(EquatableArray left, EquatableArray right) 87 | { 88 | return left.Equals(right); 89 | } 90 | 91 | /// 92 | /// Checks whether two values are not the same. 93 | /// 94 | /// The first value. 95 | /// The second value. 96 | /// Whether and are not equal. 97 | public static bool operator !=(EquatableArray left, EquatableArray right) 98 | { 99 | return !left.Equals(right); 100 | } 101 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumInFooExtensionEverythingTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foo; 3 | using Xunit; 4 | 5 | #if INTEGRATION_TESTS 6 | namespace NetEscapades.EnumGenerators.IntegrationTests; 7 | #elif NETSTANDARD_INTEGRATION_TESTS 8 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 9 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 10 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 11 | #elif INTERCEPTOR_TESTS 12 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 13 | #elif NUGET_INTEGRATION_TESTS 14 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 15 | #elif NUGET_INTERCEPTOR_TESTS 16 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 17 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 18 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 19 | #else 20 | #error Unknown integration tests 21 | #endif 22 | 23 | public class EnumInFooExtensionEverythingTests : EnumInFooExtensionsTests 24 | { 25 | protected override string[] GetNames() => EnumInFoo.GetNames(); 26 | protected override EnumInFoo[] GetValues() => EnumInFoo.GetValues(); 27 | protected override int[] GetValuesAsUnderlyingType() => EnumInFoo.GetValuesAsUnderlyingType(); 28 | protected override int AsUnderlyingValue(EnumInFoo value) => value.AsUnderlyingType(); 29 | 30 | protected override string ToStringFast(EnumInFoo value) => value.ToStringFast(); 31 | protected override string ToStringFast(EnumInFoo value, bool withMetadata) => value.ToStringFast(withMetadata); 32 | protected override bool IsDefined(EnumInFoo value) => EnumInFoo.IsDefined(value); 33 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumInFoo.IsDefined(name, allowMatchingMetadataAttribute); 34 | #if READONLYSPAN 35 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => EnumInFoo.IsDefined(name, allowMatchingMetadataAttribute); 36 | #endif 37 | protected override bool TryParse(string name, out EnumInFoo parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 38 | => EnumInFoo.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 39 | #if READONLYSPAN 40 | protected override bool TryParse(in ReadOnlySpan name, out EnumInFoo parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 41 | => EnumInFoo.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 42 | #endif 43 | protected override bool TryParse(string name, out EnumInFoo parsed, EnumParseOptions parseOptions) 44 | => EnumInFoo.TryParse(name, out parsed, parseOptions); 45 | #if READONLYSPAN 46 | protected override bool TryParse(in ReadOnlySpan name, out EnumInFoo parsed, EnumParseOptions parseOptions) 47 | => EnumInFoo.TryParse(name, out parsed, parseOptions); 48 | #endif 49 | 50 | protected override EnumInFoo Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 51 | => EnumInFoo.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 52 | #if READONLYSPAN 53 | protected override EnumInFoo Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 54 | => EnumInFoo.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 55 | #endif 56 | protected override EnumInFoo Parse(string name, EnumParseOptions parseOptions) 57 | => EnumInFoo.Parse(name, parseOptions); 58 | #if READONLYSPAN 59 | protected override EnumInFoo Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 60 | => EnumInFoo.Parse(name, parseOptions); 61 | #endif 62 | } 63 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/EquatableArray.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Immutable; 3 | 4 | namespace NetEscapades.EnumGenerators.Interceptors; 5 | 6 | /// 7 | /// An immutable, equatable array. This is equivalent to but with value equality support. 8 | /// 9 | /// The type of values in the array. 10 | public readonly struct EquatableArray : IEquatable>, IEnumerable 11 | where T : IEquatable 12 | { 13 | /// 14 | /// The underlying array. 15 | /// 16 | private readonly T[]? _array; 17 | 18 | /// 19 | /// Creates a new instance. 20 | /// 21 | /// The input to wrap. 22 | public EquatableArray(T[] array) 23 | { 24 | _array = array; 25 | } 26 | 27 | /// 28 | public bool Equals(EquatableArray array) 29 | { 30 | return AsSpan().SequenceEqual(array.AsSpan()); 31 | } 32 | 33 | /// 34 | public override bool Equals(object? obj) 35 | { 36 | return obj is EquatableArray array && this.Equals(array); 37 | } 38 | 39 | /// 40 | public override int GetHashCode() 41 | { 42 | if (_array is not T[] array) 43 | { 44 | return 0; 45 | } 46 | 47 | HashCode hashCode = default; 48 | 49 | foreach (T item in array) 50 | { 51 | hashCode.Add(item); 52 | } 53 | 54 | return hashCode.ToHashCode(); 55 | } 56 | 57 | /// 58 | /// Returns a wrapping the current items. 59 | /// 60 | /// A wrapping the current items. 61 | public ReadOnlySpan AsSpan() 62 | { 63 | return _array.AsSpan(); 64 | } 65 | 66 | /// 67 | IEnumerator IEnumerable.GetEnumerator() 68 | { 69 | return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); 70 | } 71 | 72 | /// 73 | IEnumerator IEnumerable.GetEnumerator() 74 | { 75 | return ((IEnumerable)(_array ?? Array.Empty())).GetEnumerator(); 76 | } 77 | 78 | public int Count => _array?.Length ?? 0; 79 | 80 | /// 81 | /// Checks whether two values are the same. 82 | /// 83 | /// The first value. 84 | /// The second value. 85 | /// Whether and are equal. 86 | public static bool operator ==(EquatableArray left, EquatableArray right) 87 | { 88 | return left.Equals(right); 89 | } 90 | 91 | /// 92 | /// Checks whether two values are not the same. 93 | /// 94 | /// The first value. 95 | /// The second value. 96 | /// Whether and are not equal. 97 | public static bool operator !=(EquatableArray left, EquatableArray right) 98 | { 99 | return !left.Equals(right); 100 | } 101 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Tests/SourceGenerationHelperSnapshotTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using VerifyXunit; 5 | using Xunit; 6 | 7 | namespace NetEscapades.EnumGenerators.Tests; 8 | 9 | [UsesVerify] 10 | public class SourceGenerationHelperSnapshotTests 11 | { 12 | private const MetadataSource DefaultMetadataSource = MetadataSource.EnumMemberAttribute; 13 | 14 | [Theory] 15 | [CombinatorialData] 16 | public Task GeneratesEnumCorrectly( 17 | bool csharp14IsSupported, 18 | [CombinatorialValues(MetadataSource.None, MetadataSource.EnumMemberAttribute)] 19 | MetadataSource defaultSource, 20 | bool useCollectionExpressions) 21 | { 22 | var value = new EnumToGenerate( 23 | "ShortName", 24 | "Something.Blah", 25 | "Something.Blah.ShortName", 26 | "int", 27 | isPublic: true, 28 | new List<(string Key, EnumValueOption Value)> 29 | { 30 | ("First", EnumValueOption.CreateWithoutAttributes(0)), 31 | ("Second", EnumValueOption.CreateWithoutAttributes(1)), 32 | }, 33 | hasFlags: false, 34 | metadataSource: null); 35 | 36 | var result = SourceGenerationHelper.GenerateExtensionClass( 37 | value, 38 | csharp14IsSupported, 39 | useCollectionExpressions: useCollectionExpressions, 40 | defaultSource).Content; 41 | 42 | return Verifier.Verify(result) 43 | .ScrubExpectedChanges() 44 | .UseDirectory("Snapshots") 45 | .UseTextForParameters($"{csharp14IsSupported}_{defaultSource}_{useCollectionExpressions}"); 46 | } 47 | 48 | [Theory] 49 | [CombinatorialData] 50 | public Task GeneratesEnumWithRepeatedValuesCorrectly(bool csharp14IsSupported) 51 | { 52 | var value = new EnumToGenerate( 53 | "ShortName", 54 | "Something.Blah", 55 | "Something.Blah.ShortName", 56 | "int", 57 | isPublic: true, 58 | new List<(string Key, EnumValueOption Value)> 59 | { 60 | ("First", EnumValueOption.CreateWithoutAttributes(0)), 61 | ("Second", EnumValueOption.CreateWithoutAttributes(1)), 62 | ("Third", EnumValueOption.CreateWithoutAttributes(0)), 63 | }, 64 | hasFlags: false, 65 | null); 66 | 67 | var result = SourceGenerationHelper.GenerateExtensionClass(value, csharp14IsSupported, 68 | useCollectionExpressions: false, DefaultMetadataSource).Content; 69 | 70 | return Verifier.Verify(result) 71 | .ScrubExpectedChanges() 72 | .UseDirectory("Snapshots") 73 | .UseParameters(csharp14IsSupported); 74 | } 75 | 76 | [Theory] 77 | [CombinatorialData] 78 | public Task GeneratesFlagsEnumCorrectly(bool csharp14IsSupported) 79 | { 80 | var value = new EnumToGenerate( 81 | "ShortName", 82 | "Something.Blah", 83 | "Something.Blah.ShortName", 84 | "int", 85 | isPublic: true, 86 | new List<(string, EnumValueOption)> 87 | { 88 | ("First", EnumValueOption.CreateWithoutAttributes(0)), 89 | ("Second", EnumValueOption.CreateWithoutAttributes(1)), 90 | }, 91 | hasFlags: true, 92 | metadataSource: null); 93 | 94 | var result = SourceGenerationHelper.GenerateExtensionClass(value, csharp14IsSupported, 95 | useCollectionExpressions: false, DefaultMetadataSource).Content; 96 | 97 | return Verifier.Verify(result) 98 | .ScrubExpectedChanges() 99 | .UseDirectory("Snapshots") 100 | .UseParameters(csharp14IsSupported); 101 | } 102 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Attributes/EnumExtensionsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NetEscapades.EnumGenerators 2 | { 3 | /// 4 | /// Add to enums to indicate that extension methods should be generated for the type 5 | /// 6 | [System.AttributeUsage(System.AttributeTargets.Enum)] 7 | [System.Diagnostics.Conditional("NETESCAPADES_ENUMGENERATORS_USAGES")] 8 | public class EnumExtensionsAttribute : System.Attribute 9 | { 10 | /// 11 | /// The namespace to generate the extension class. 12 | /// If not provided, the namespace of the enum will be used. 13 | /// 14 | public string? ExtensionClassNamespace { get; set; } 15 | 16 | /// 17 | /// The name to use for the extension class. 18 | /// If not provided, the enum name with an Extensions suffix will be used. 19 | /// For example for an Enum called StatusCodes, the default name 20 | /// will be StatusCodesExtensions. 21 | /// 22 | public string? ExtensionClassName { get; set; } 23 | 24 | /// 25 | /// The metadata source to use when serializing and deserializing using 26 | /// ToStringFast() and TryParse(). If not provided 27 | /// will be 28 | /// used to provide the values. Alternatively, you can disable this feature 29 | /// entirely by using . 30 | /// 31 | public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute; 32 | 33 | /// 34 | /// By default, when used with NetEscapades.EnumGenerators.Interceptors 35 | /// any interceptable usages of the enum will be replaced by usages of 36 | /// the extension method in this project. To disable interception of 37 | /// the enum in this project when used with the interceptable package, 38 | /// set to . 39 | /// 40 | public bool IsInterceptable { get; set; } = true; 41 | } 42 | 43 | /// 44 | /// Add to enums to indicate that extension methods should be generated for the type 45 | /// 46 | [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] 47 | [System.Diagnostics.Conditional("NETESCAPADES_ENUMGENERATORS_USAGES")] 48 | public class EnumExtensionsAttribute : System.Attribute 49 | where T: System.Enum 50 | { 51 | /// 52 | /// The namespace to generate the extension class. 53 | /// If not provided, the namespace of the enum will be used. 54 | /// 55 | public string? ExtensionClassNamespace { get; set; } 56 | 57 | /// 58 | /// The name to use for the extension class. 59 | /// If not provided, the enum name with an Extensions suffix will be used. 60 | /// For example for an Enum called StatusCodes, the default name 61 | /// will be StatusCodesExtensions. 62 | /// 63 | public string? ExtensionClassName { get; set; } 64 | 65 | /// 66 | /// The metadata source to use when serializing and deserializing using 67 | /// ToStringFast() and TryParse(). If not provided, the 68 | /// will be 69 | /// used to provide the values. Alternatively, you can disable this feature 70 | /// entirely by using . 71 | /// 72 | public MetadataSource MetadataSource { get; set; } = MetadataSource.EnumMemberAttribute; 73 | 74 | /// 75 | /// By default, when used with NetEscapades.EnumGenerators.Interceptors 76 | /// any interceptable usages of the enum will be replaced by usages of 77 | /// the extension method in this project. To disable interception of 78 | /// the enum in this project when used with the interceptable package, 79 | /// set to . 80 | /// 81 | public bool IsInterceptable { get; set; } = true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/HasFlagAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | namespace NetEscapades.EnumGenerators.Diagnostics; 8 | 9 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 10 | public class HasFlagAnalyzer : DiagnosticAnalyzer 11 | { 12 | public const string DiagnosticId = "NEEG005"; 13 | public static readonly DiagnosticDescriptor Rule = new( 14 | #pragma warning disable RS2008 // Enable Analyzer Release Tracking 15 | id: DiagnosticId, 16 | #pragma warning restore RS2008 17 | title: "Use HasFlagFast() instead of HasFlag()", 18 | messageFormat: "Use HasFlagFast() instead of HasFlag() for better performance on enum '{0}'", 19 | category: "Usage", 20 | defaultSeverity: DiagnosticSeverity.Info, 21 | isEnabledByDefault: true); 22 | 23 | public override ImmutableArray SupportedDiagnostics 24 | => ImmutableArray.Create(Rule); 25 | 26 | public override void Initialize(AnalysisContext context) 27 | { 28 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 29 | context.EnableConcurrentExecution(); 30 | context.RegisterCompilationStartAction(ctx => 31 | { 32 | var (enumExtensionsAttr, externalEnumTypes) = AnalyzerHelpers.GetEnumExtensionAttributes(ctx.Compilation); 33 | if (enumExtensionsAttr is null || externalEnumTypes is null) 34 | { 35 | return; 36 | } 37 | 38 | ctx.RegisterSyntaxNodeAction( 39 | c => AnalyzeInvocation(c, enumExtensionsAttr, externalEnumTypes), 40 | SyntaxKind.InvocationExpression); 41 | }); 42 | } 43 | 44 | private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context, INamedTypeSymbol enumExtensionsAttr, ExternalEnumDictionary externalEnumTypes) 45 | { 46 | var invocation = (InvocationExpressionSyntax)context.Node; 47 | 48 | // Check if this is a member access expression (e.g., value.HasFlag()) 49 | if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) 50 | { 51 | return; 52 | } 53 | 54 | // Check if the method name is "HasFlag" 55 | if (memberAccess.Name.Identifier.Text != "HasFlag") 56 | { 57 | return; 58 | } 59 | 60 | // Check if there is exactly one argument 61 | if (invocation.ArgumentList.Arguments.Count != 1) 62 | { 63 | return; 64 | } 65 | 66 | // Get the symbol information for the invocation 67 | var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); 68 | if (symbolInfo.Symbol is not IMethodSymbol methodSymbol) 69 | { 70 | return; 71 | } 72 | 73 | // Verify this is the HasFlag() method from System.Enum 74 | if (methodSymbol.Name != "HasFlag" || 75 | methodSymbol.Parameters.Length != 1 || 76 | methodSymbol.ContainingType.SpecialType != SpecialType.System_Enum) 77 | { 78 | return; 79 | } 80 | 81 | // Get the type of the receiver (the thing before .HasFlag()) 82 | var receiverType = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; 83 | if (receiverType is null || receiverType.TypeKind != TypeKind.Enum) 84 | { 85 | return; 86 | } 87 | 88 | if (!AnalyzerHelpers.IsEnumWithExtensions(receiverType, enumExtensionsAttr, externalEnumTypes, out var extensionNamespace, out var extensionClass)) 89 | { 90 | return; 91 | } 92 | 93 | // Report the diagnostic 94 | var diagnostic = Diagnostic.Create( 95 | descriptor: Rule, 96 | location: memberAccess.Name.GetLocation(), 97 | messageArgs: receiverType.Name, 98 | properties: ImmutableDictionary.CreateRange([ 99 | new(nameof(EnumExtensionsAttribute.ExtensionClassNamespace), extensionNamespace), 100 | new(nameof(EnumExtensionsAttribute.ExtensionClassName), extensionClass), 101 | ])); 102 | 103 | context.ReportDiagnostic(diagnostic); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumInFooExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Foo; 3 | using Xunit; 4 | 5 | #if INTEGRATION_TESTS 6 | namespace NetEscapades.EnumGenerators.IntegrationTests; 7 | #elif NETSTANDARD_INTEGRATION_TESTS 8 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 9 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 10 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 11 | #elif INTERCEPTOR_TESTS 12 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 13 | #elif NUGET_INTEGRATION_TESTS 14 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 15 | #elif NUGET_INTERCEPTOR_TESTS 16 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 17 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 18 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 19 | #else 20 | #error Unknown integration tests 21 | #endif 22 | 23 | public class EnumInFooExtensionsTests : ExtensionTests, ITestData 24 | { 25 | public TheoryData ValidEnumValues() => new() 26 | { 27 | EnumInFoo.First, 28 | EnumInFoo.Second, 29 | (EnumInFoo)3, 30 | }; 31 | 32 | public TheoryData ValuesToParse() => new() 33 | { 34 | "First", 35 | "Second", 36 | "2nd", 37 | "2ND", 38 | "first", 39 | "SECOND", 40 | "3", 41 | "267", 42 | "-267", 43 | "2147483647", 44 | "3000000000", 45 | "Fourth", 46 | "Fifth", 47 | }; 48 | 49 | protected override string[] GetNames() => EnumInFooExtensions.GetNames(); 50 | protected override EnumInFoo[] GetValues() => EnumInFooExtensions.GetValues(); 51 | protected override int[] GetValuesAsUnderlyingType() => EnumInFooExtensions.GetValuesAsUnderlyingType(); 52 | protected override int AsUnderlyingValue(EnumInFoo value) => value.AsUnderlyingType(); 53 | 54 | protected override string ToStringFast(EnumInFoo value) => value.ToStringFast(); 55 | protected override string ToStringFast(EnumInFoo value, bool withMetadata) => value.ToStringFast(withMetadata); 56 | protected override string ToStringFast(EnumInFoo value, SerializationOptions options) => value.ToStringFast(options); 57 | protected override bool IsDefined(EnumInFoo value) => EnumInFooExtensions.IsDefined(value); 58 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumInFooExtensions.IsDefined(name, allowMatchingMetadataAttribute); 59 | #if READONLYSPAN 60 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => EnumInFooExtensions.IsDefined(name, allowMatchingMetadataAttribute); 61 | #endif 62 | protected override bool TryParse(string name, out EnumInFoo parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 63 | => EnumInFooExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 64 | #if READONLYSPAN 65 | protected override bool TryParse(in ReadOnlySpan name, out EnumInFoo parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 66 | => EnumInFooExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 67 | #endif 68 | protected override bool TryParse(string name, out EnumInFoo parsed, EnumParseOptions parseOptions) 69 | => EnumInFooExtensions.TryParse(name, out parsed, parseOptions); 70 | #if READONLYSPAN 71 | protected override bool TryParse(in ReadOnlySpan name, out EnumInFoo parsed, EnumParseOptions parseOptions) 72 | => EnumInFooExtensions.TryParse(name, out parsed, parseOptions); 73 | #endif 74 | 75 | protected override EnumInFoo Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 76 | => EnumInFooExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 77 | #if READONLYSPAN 78 | protected override EnumInFoo Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 79 | => EnumInFooExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 80 | #endif 81 | protected override EnumInFoo Parse(string name, EnumParseOptions parseOptions) 82 | => EnumInFooExtensions.Parse(name, parseOptions); 83 | #if READONLYSPAN 84 | protected override EnumInFoo Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 85 | => EnumInFooExtensions.Parse(name, parseOptions); 86 | #endif 87 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/LongEnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NetEscapades.EnumGenerators; 3 | using Xunit; 4 | 5 | #if INTEGRATION_TESTS 6 | namespace NetEscapades.EnumGenerators.IntegrationTests; 7 | #elif NETSTANDARD_INTEGRATION_TESTS 8 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 9 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 10 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 11 | #elif INTERCEPTOR_TESTS 12 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 13 | #elif NUGET_INTEGRATION_TESTS 14 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 15 | #elif NUGET_INTERCEPTOR_TESTS 16 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 17 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 18 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 19 | #else 20 | #error Unknown integration tests 21 | #endif 22 | 23 | public class LongEnumExtensionsTests : ExtensionTests, ITestData 24 | { 25 | public TheoryData ValidEnumValues() => new() 26 | { 27 | LongEnum.First, 28 | LongEnum.Second, 29 | (LongEnum)3, 30 | }; 31 | 32 | public TheoryData ValuesToParse() => new() 33 | { 34 | "First", 35 | "Second", 36 | "2nd", 37 | "2ND", 38 | "first", 39 | "SECOND", 40 | "3", 41 | "267", 42 | "-267", 43 | "2147483647", 44 | "3000000000", 45 | "Fourth", 46 | "Fifth", 47 | }; 48 | 49 | protected override string[] GetNames() => LongEnumExtensions.GetNames(); 50 | protected override LongEnum[] GetValues() => LongEnumExtensions.GetValues(); 51 | protected override long[] GetValuesAsUnderlyingType() => LongEnumExtensions.GetValuesAsUnderlyingType(); 52 | protected override long AsUnderlyingValue(LongEnum value) => value.AsUnderlyingType(); 53 | 54 | protected override string ToStringFast(LongEnum value) => value.ToStringFast(); 55 | protected override string ToStringFast(LongEnum value, bool withMetadata) => value.ToStringFast(withMetadata); 56 | protected override string ToStringFast(LongEnum value, SerializationOptions options) => value.ToStringFast(options); 57 | protected override bool IsDefined(LongEnum value) => LongEnumExtensions.IsDefined(value); 58 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => LongEnumExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 59 | #if READONLYSPAN 60 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => LongEnumExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 61 | #endif 62 | protected override bool TryParse(string name, out LongEnum parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 63 | => LongEnumExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 64 | #if READONLYSPAN 65 | protected override bool TryParse(in ReadOnlySpan name, out LongEnum parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 66 | => LongEnumExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 67 | #endif 68 | protected override bool TryParse(string name, out LongEnum parsed, EnumParseOptions parseOptions) 69 | => LongEnumExtensions.TryParse(name, out parsed, parseOptions); 70 | #if READONLYSPAN 71 | protected override bool TryParse(in ReadOnlySpan name, out LongEnum parsed, EnumParseOptions parseOptions) 72 | => LongEnumExtensions.TryParse(name, out parsed, parseOptions); 73 | #endif 74 | 75 | protected override LongEnum Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 76 | => LongEnumExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 77 | #if READONLYSPAN 78 | protected override LongEnum Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 79 | => LongEnumExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 80 | #endif 81 | protected override LongEnum Parse(string name, EnumParseOptions parseOptions) 82 | => LongEnumExtensions.Parse(name, parseOptions); 83 | #if READONLYSPAN 84 | protected override LongEnum Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 85 | => LongEnumExtensions.Parse(name, parseOptions); 86 | #endif 87 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/AnalyzerHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace NetEscapades.EnumGenerators.Diagnostics; 5 | 6 | public static class AnalyzerHelpers 7 | { 8 | public static (INamedTypeSymbol? enumExtensionsAttr, ExternalEnumDictionary? externalEnumTypes) GetEnumExtensionAttributes(Compilation compilation) 9 | { 10 | var enumExtensionsAttr = 11 | compilation.GetTypeByMetadataName(Attributes.EnumExtensionsAttribute); 12 | var externalEnumExtensionsAttr = 13 | compilation.GetTypeByMetadataName(Attributes.ExternalEnumExtensionsAttribute); 14 | 15 | if (enumExtensionsAttr is null) 16 | { 17 | return (enumExtensionsAttr, null); 18 | } 19 | 20 | // Collect all enum types that have EnumExtensions attributes 21 | var externalEnumTypes = new ExternalEnumDictionary(SymbolEqualityComparer.Default); 22 | if (externalEnumExtensionsAttr is not null) 23 | { 24 | foreach (var attribute in compilation.Assembly.GetAttributes()) 25 | { 26 | if (attribute.AttributeClass is { IsGenericType: true } attrClass && 27 | SymbolEqualityComparer.Default.Equals(attrClass.ConstructedFrom, externalEnumExtensionsAttr) && 28 | attrClass.TypeArguments is [INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType]) 29 | { 30 | var details = ExtractExtensionClassDetails(enumType, attribute); 31 | externalEnumTypes.Add(enumType, details); 32 | } 33 | } 34 | } 35 | 36 | return (enumExtensionsAttr, externalEnumTypes); 37 | } 38 | 39 | public static bool IsEnumWithExtensions( 40 | ITypeSymbol receiverType, 41 | INamedTypeSymbol enumExtensionsAttr, 42 | ExternalEnumDictionary externalEnumTypes, 43 | [NotNullWhen(true)] out string? extensionNamespace, 44 | [NotNullWhen(true)] out string? extensionClass) 45 | { 46 | // Check if the enum has the [EnumExtensions] attribute or is referenced in EnumExtensions 47 | // First check if the enum itself has the attribute 48 | foreach (var attributeData in receiverType.GetAttributes()) 49 | { 50 | if (SymbolEqualityComparer.Default.Equals( 51 | attributeData.AttributeClass, 52 | enumExtensionsAttr)) 53 | { 54 | (extensionNamespace, extensionClass) = ExtractExtensionClassDetails(receiverType, attributeData); 55 | return true; 56 | } 57 | } 58 | 59 | // If not, check if it's in the external enum types (EnumExtensions) 60 | if(receiverType is INamedTypeSymbol namedType 61 | && externalEnumTypes.TryGetValue(namedType, out var details)) 62 | { 63 | extensionNamespace = details.Namespace; 64 | extensionClass = details.Class; 65 | return true; 66 | } 67 | 68 | extensionNamespace = null; 69 | extensionClass = null; 70 | return false; 71 | } 72 | 73 | private static (string Namespace, string Class) ExtractExtensionClassDetails( 74 | ITypeSymbol receiverType, 75 | AttributeData attributeData) 76 | { 77 | string? nameSpace = null; 78 | string? className = null; 79 | 80 | if (!attributeData.NamedArguments.IsDefaultOrEmpty) 81 | { 82 | // extract the extension namespace 83 | foreach (var (key, value) in attributeData.NamedArguments) 84 | { 85 | if (key == nameof(EnumExtensionsAttribute.ExtensionClassNamespace) && 86 | value is { Kind: TypedConstantKind.Primitive, Value: string ns } && 87 | !string.IsNullOrWhiteSpace(ns)) 88 | { 89 | nameSpace = ns; 90 | } 91 | if (key == nameof(EnumExtensionsAttribute.ExtensionClassName) && 92 | value is { Kind: TypedConstantKind.Primitive, Value: string name } && 93 | !string.IsNullOrWhiteSpace(name)) 94 | { 95 | className = name; 96 | } 97 | } 98 | } 99 | 100 | return (Namespace: nameSpace ?? EnumGenerator.GetEnumExtensionNamespace(receiverType), 101 | Class: className ?? EnumGenerator.GetEnumExtensionName(receiverType)); 102 | } 103 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumInNamespaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumInNamespaceExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumInNamespace.First, 27 | EnumInNamespace.Second, 28 | (EnumInNamespace)3, 29 | }; 30 | 31 | public TheoryData ValuesToParse() => new() 32 | { 33 | "First", 34 | "Second", 35 | "2nd", 36 | "2ND", 37 | "first", 38 | "SECOND", 39 | "3", 40 | "267", 41 | "-267", 42 | "2147483647", 43 | "3000000000", 44 | "Fourth", 45 | "Fifth", 46 | }; 47 | 48 | protected override string[] GetNames() => EnumInNamespaceExtensions.GetNames(); 49 | protected override EnumInNamespace[] GetValues() => EnumInNamespaceExtensions.GetValues(); 50 | protected override int[] GetValuesAsUnderlyingType() => EnumInNamespaceExtensions.GetValuesAsUnderlyingType(); 51 | protected override int AsUnderlyingValue(EnumInNamespace value) => value.AsUnderlyingType(); 52 | 53 | protected override string ToStringFast(EnumInNamespace value) => value.ToStringFast(); 54 | protected override string ToStringFast(EnumInNamespace value, bool withMetadata) => value.ToStringFast(withMetadata); 55 | protected override string ToStringFast(EnumInNamespace value, SerializationOptions options) => value.ToStringFast(options); 56 | protected override bool IsDefined(EnumInNamespace value) => EnumInNamespaceExtensions.IsDefined(value); 57 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 58 | #if READONLYSPAN 59 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => EnumInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 60 | #endif 61 | protected override bool TryParse(string name, out EnumInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #if READONLYSPAN 64 | protected override bool TryParse(in ReadOnlySpan name, out EnumInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => EnumInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #endif 67 | protected override bool TryParse(string name, out EnumInNamespace parsed, EnumParseOptions parseOptions) 68 | => EnumInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 69 | #if READONLYSPAN 70 | protected override bool TryParse(in ReadOnlySpan name, out EnumInNamespace parsed, EnumParseOptions parseOptions) 71 | => EnumInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 72 | #endif 73 | 74 | protected override EnumInNamespace Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #if READONLYSPAN 77 | protected override EnumInNamespace Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => EnumInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #endif 80 | protected override EnumInNamespace Parse(string name, EnumParseOptions parseOptions) 81 | => EnumInNamespaceExtensions.Parse(name, parseOptions); 82 | #if READONLYSPAN 83 | protected override EnumInNamespace Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 84 | => EnumInNamespaceExtensions.Parse(name, parseOptions); 85 | #endif 86 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/ExternalEnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Foo; 4 | using Xunit; 5 | 6 | #if INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.IntegrationTests; 8 | #elif NETSTANDARD_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 10 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 11 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 12 | #elif INTERCEPTOR_TESTS 13 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 14 | #elif NUGET_INTEGRATION_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 16 | #elif NUGET_INTERCEPTOR_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 18 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 19 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 20 | #else 21 | #error Unknown integration tests 22 | #endif 23 | 24 | public class ExternalEnumExtensionsTests : ExtensionTests, ITestData 25 | { 26 | public TheoryData ValidEnumValues() => new() 27 | { 28 | DateTimeKind.Unspecified, 29 | DateTimeKind.Utc, 30 | (DateTimeKind)3, // not actually valid 31 | }; 32 | 33 | public TheoryData ValuesToParse() => new() 34 | { 35 | "Unspecified", 36 | "Utc", 37 | "Local", 38 | "NotLocal", 39 | "SomethingElse", 40 | "utc", 41 | "UTC", 42 | "3", 43 | "267", 44 | "-267", 45 | "2147483647", 46 | "3000000000", 47 | "Fourth", 48 | "Fifth", 49 | }; 50 | 51 | protected override string[] GetNames() => DateTimeKindExtensions.GetNames(); 52 | protected override DateTimeKind[] GetValues() => DateTimeKindExtensions.GetValues(); 53 | protected override int[] GetValuesAsUnderlyingType() => DateTimeKindExtensions.GetValuesAsUnderlyingType(); 54 | protected override int AsUnderlyingValue(DateTimeKind value) => value.AsUnderlyingType(); 55 | 56 | protected override string ToStringFast(DateTimeKind value) => value.ToStringFast(); 57 | protected override string ToStringFast(DateTimeKind value, bool withMetadata) => value.ToStringFast(withMetadata); 58 | protected override string ToStringFast(DateTimeKind value, SerializationOptions options) => value.ToStringFast(options); 59 | protected override bool IsDefined(DateTimeKind value) => DateTimeKindExtensions.IsDefined(value); 60 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => DateTimeKindExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 61 | #if READONLYSPAN 62 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => DateTimeKindExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 63 | #endif 64 | protected override bool TryParse(string name, out DateTimeKind parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => DateTimeKindExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #if READONLYSPAN 67 | protected override bool TryParse(in ReadOnlySpan name, out DateTimeKind parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 68 | => DateTimeKindExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 69 | #endif 70 | protected override bool TryParse(string name, out DateTimeKind parsed, EnumParseOptions parseOptions) 71 | => DateTimeKindExtensions.TryParse(name, out parsed, parseOptions); 72 | #if READONLYSPAN 73 | protected override bool TryParse(in ReadOnlySpan name, out DateTimeKind parsed, EnumParseOptions parseOptions) 74 | => DateTimeKindExtensions.TryParse(name, out parsed, parseOptions); 75 | #endif 76 | 77 | protected override DateTimeKind Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => DateTimeKindExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #if READONLYSPAN 80 | protected override DateTimeKind Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 81 | => DateTimeKindExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 82 | #endif 83 | protected override DateTimeKind Parse(string name, EnumParseOptions parseOptions) 84 | => DateTimeKindExtensions.Parse(name, parseOptions); 85 | #if READONLYSPAN 86 | protected override DateTimeKind Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 87 | => DateTimeKindExtensions.Parse(name, parseOptions); 88 | #endif 89 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/DuplicateEnumValueAnalyzer.cs: -------------------------------------------------------------------------------- 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 DuplicateEnumValueAnalyzer : DiagnosticAnalyzer 12 | { 13 | public const string DiagnosticId = "NEEG003"; 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: "Enum has duplicate values and will give inconsistent values for ToStringFast()", 19 | messageFormat: "The enum member '{0}' has the same value as a previous member so will return the '{1}' value for ToStringFast()", 20 | category: "Usage", 21 | defaultSeverity: DiagnosticSeverity.Info, 22 | isEnabledByDefault: true); 23 | 24 | public override ImmutableArray 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 void AnalyzeEnumDeclaration(SyntaxNodeAnalysisContext context) 35 | { 36 | var enumDeclaration = (EnumDeclarationSyntax)context.Node; 37 | 38 | // Check if enum has [EnumExtensions] attribute 39 | bool hasEnumExtensionsAttribute = false; 40 | foreach (var attributeList in enumDeclaration.AttributeLists) 41 | { 42 | foreach (var attribute in attributeList.Attributes) 43 | { 44 | // Check attribute name syntactically first 45 | var attributeName = attribute.Name.ToString(); 46 | if (attributeName == "EnumExtensions" || attributeName == "EnumExtensionsAttribute") 47 | { 48 | // Verify with semantic model for precision 49 | var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute); 50 | if (symbolInfo.Symbol is IMethodSymbol method && 51 | method.ContainingType.ToDisplayString() == Attributes.EnumExtensionsAttribute) 52 | { 53 | hasEnumExtensionsAttribute = true; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | if (hasEnumExtensionsAttribute) 60 | { 61 | break; 62 | } 63 | } 64 | 65 | if (!hasEnumExtensionsAttribute) 66 | { 67 | return; 68 | } 69 | 70 | // Get the enum symbol 71 | var enumSymbol = context.SemanticModel.GetDeclaredSymbol(enumDeclaration); 72 | if (enumSymbol is null) 73 | { 74 | return; 75 | } 76 | 77 | // Track which constant values we've seen and the first name 78 | var seenValues = new Dictionary(); 79 | 80 | // Analyze each enum member 81 | foreach (var member in enumSymbol.GetMembers().OfType()) 82 | { 83 | if (!member.IsConst || member.ConstantValue is null) 84 | { 85 | continue; 86 | } 87 | 88 | var constantValue = member.ConstantValue; 89 | var memberName = member.Name; 90 | 91 | // If we've already seen this value, this member will be excluded 92 | if (seenValues.TryGetValue(constantValue, out var previousName)) 93 | { 94 | // Find the syntax location for this member 95 | var memberLocation = member.Locations.FirstOrDefault(); 96 | if (memberLocation is not null) 97 | { 98 | var diagnostic = Diagnostic.Create( 99 | Rule, 100 | memberLocation, 101 | memberName, 102 | previousName); 103 | 104 | context.ReportDiagnostic(diagnostic); 105 | } 106 | } 107 | else 108 | { 109 | seenValues[member.ConstantValue] = memberName; 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators/Diagnostics/DuplicateExtensionClassAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Immutable; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | namespace NetEscapades.EnumGenerators.Diagnostics; 7 | 8 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 9 | public class DuplicateExtensionClassAnalyzer: DiagnosticAnalyzer 10 | { 11 | public const string DiagnosticId = "NEEG001"; 12 | private static readonly DiagnosticDescriptor Rule = new( 13 | #pragma warning disable RS2008 // Enable Analyzer Release Tracking 14 | id: DiagnosticId, 15 | #pragma warning restore RS2008 16 | title: "Duplicate generated extension class", 17 | messageFormat: 18 | "The generated extension class '{1}.{2}' for enum '{0}' clashes with other generated extension classes. Use ExtensionClassNamespace or ExtensionClassName to specify a unique combination.", 19 | category: "Usage", 20 | defaultSeverity: DiagnosticSeverity.Error, 21 | isEnabledByDefault: true, 22 | customTags: "CompilationEnd"); 23 | 24 | public override ImmutableArray SupportedDiagnostics => 25 | ImmutableArray.Create(Rule); 26 | 27 | public override void Initialize(AnalysisContext context) 28 | { 29 | // Analyze symbols instead of syntax 30 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 31 | context.EnableConcurrentExecution(); 32 | 33 | #pragma warning disable RS1012 // 'startContext' does not register any analyzer actions - false positive 34 | context.RegisterCompilationStartAction(startContext => 35 | #pragma warning restore RS1012 // 'startContext' does not register any analyzer actions - false positive 36 | { 37 | var enumMap = new ConcurrentDictionary, List>>(); 38 | 39 | startContext.RegisterSymbolAction(symbolContext => 40 | { 41 | var ct = symbolContext.CancellationToken; 42 | var enumSymbol = (INamedTypeSymbol)symbolContext.Symbol; 43 | if (enumSymbol.TypeKind != TypeKind.Enum) 44 | { 45 | return; 46 | } 47 | 48 | Location? location = null; 49 | string? ns = null; 50 | string? name = null; 51 | MetadataSource? source = null; 52 | foreach (var attributeData in enumSymbol.GetAttributes()) 53 | { 54 | if (ct.IsCancellationRequested) 55 | { 56 | return; 57 | } 58 | 59 | if (EnumGenerator.TryGetExtensionAttributeDetails(attributeData, ref ns, ref name, ref source)) 60 | { 61 | location = attributeData.ApplicationSyntaxReference?.GetSyntax(ct).GetLocation() 62 | ?? enumSymbol.Locations[0]; 63 | break; 64 | } 65 | } 66 | 67 | if (location is null || ct.IsCancellationRequested) 68 | { 69 | return; 70 | } 71 | 72 | // we have the attribute, get the calculated names 73 | ns ??= EnumGenerator.GetEnumExtensionNamespace(enumSymbol); 74 | name ??= EnumGenerator.GetEnumExtensionName(enumSymbol); 75 | 76 | enumMap.AddOrUpdate(new(ns, name), 77 | _ => [new(location, enumSymbol.Name)], 78 | (_, list) => 79 | { 80 | list.Add(new(location, enumSymbol.Name)); 81 | return list; 82 | }); 83 | }, SymbolKind.NamedType); 84 | 85 | startContext.RegisterCompilationEndAction(endContext => 86 | { 87 | foreach (var kvp in enumMap) 88 | { 89 | var duplicates = kvp.Value; 90 | if (duplicates.Count > 1) 91 | { 92 | foreach (var symbol in duplicates) 93 | { 94 | var ns = kvp.Key.Item1; 95 | var name = kvp.Key.Item2; 96 | var diag = Diagnostic.Create(Rule, symbol.Item1, 97 | symbol.Item2, ns, name); 98 | endContext.ReportDiagnostic(diag); 99 | } 100 | } 101 | } 102 | }); 103 | }); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithReservedKeywordsExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumWithReservedKeywordsExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumWithReservedKeywords.number, 27 | EnumWithReservedKeywords.@string, 28 | EnumWithReservedKeywords.date, 29 | EnumWithReservedKeywords.@class, 30 | }; 31 | 32 | public TheoryData ValuesToParse() => new() 33 | { 34 | "number", 35 | "string", 36 | "date", 37 | "class", 38 | "NUMBER", 39 | "STRING", 40 | "1", 41 | "2", 42 | "-1", 43 | }; 44 | 45 | protected override string[] GetNames() => EnumWithReservedKeywordsExtensions.GetNames(); 46 | protected override EnumWithReservedKeywords[] GetValues() => EnumWithReservedKeywordsExtensions.GetValues(); 47 | protected override int[] GetValuesAsUnderlyingType() => EnumWithReservedKeywordsExtensions.GetValuesAsUnderlyingType(); 48 | protected override int AsUnderlyingValue(EnumWithReservedKeywords value) => value.AsUnderlyingType(); 49 | 50 | protected override string ToStringFast(EnumWithReservedKeywords value) => value.ToStringFast(); 51 | protected override string ToStringFast(EnumWithReservedKeywords value, bool withMetadata) => value.ToStringFast(withMetadata); 52 | protected override string ToStringFast(EnumWithReservedKeywords value, SerializationOptions options) => value.ToStringFast(options); 53 | protected override bool IsDefined(EnumWithReservedKeywords value) => EnumWithReservedKeywordsExtensions.IsDefined(value); 54 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithReservedKeywordsExtensions.IsDefined(name, allowMatchingMetadataAttribute); 55 | #if READONLYSPAN 56 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute = false) => EnumWithReservedKeywordsExtensions.IsDefined(name, allowMatchingMetadataAttribute); 57 | #endif 58 | protected override bool TryParse(string name, out EnumWithReservedKeywords parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 59 | => EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 60 | #if READONLYSPAN 61 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithReservedKeywords parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #endif 64 | protected override bool TryParse(string name, out EnumWithReservedKeywords parsed, EnumParseOptions parseOptions) 65 | => EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, parseOptions); 66 | #if READONLYSPAN 67 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithReservedKeywords parsed, EnumParseOptions parseOptions) 68 | => EnumWithReservedKeywordsExtensions.TryParse(name, out parsed, parseOptions); 69 | #endif 70 | 71 | protected override EnumWithReservedKeywords Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 72 | => EnumWithReservedKeywordsExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 73 | #if READONLYSPAN 74 | protected override EnumWithReservedKeywords Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumWithReservedKeywordsExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #endif 77 | protected override EnumWithReservedKeywords Parse(string name, EnumParseOptions parseOptions) 78 | => EnumWithReservedKeywordsExtensions.Parse(name, parseOptions); 79 | #if READONLYSPAN 80 | protected override EnumWithReservedKeywords Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 81 | => EnumWithReservedKeywordsExtensions.Parse(name, parseOptions); 82 | #endif 83 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithSameDisplayNameExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumWithSameDisplayNameExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumWithSameDisplayName.First, 27 | EnumWithSameDisplayName.Second, 28 | (EnumWithSameDisplayName)3, 29 | }; 30 | 31 | public TheoryData ValuesToParse() => new() 32 | { 33 | "First", 34 | "Second", 35 | "2nd", 36 | "2ND", 37 | "first", 38 | "SECOND", 39 | "3", 40 | "267", 41 | "-267", 42 | "2147483647", 43 | "3000000000", 44 | "Fourth", 45 | "Fifth", 46 | }; 47 | 48 | protected override string[] GetNames() => EnumWithSameDisplayNameExtensions.GetNames(); 49 | protected override EnumWithSameDisplayName[] GetValues() => EnumWithSameDisplayNameExtensions.GetValues(); 50 | protected override int[] GetValuesAsUnderlyingType() => EnumWithSameDisplayNameExtensions.GetValuesAsUnderlyingType(); 51 | protected override int AsUnderlyingValue(EnumWithSameDisplayName value) => value.AsUnderlyingType(); 52 | 53 | protected override string ToStringFast(EnumWithSameDisplayName value) => value.ToStringFast(); 54 | protected override string ToStringFast(EnumWithSameDisplayName value, bool withMetadata) => value.ToStringFast(withMetadata); 55 | protected override string ToStringFast(EnumWithSameDisplayName value, SerializationOptions options) => value.ToStringFast(options); 56 | protected override bool IsDefined(EnumWithSameDisplayName value) => EnumWithSameDisplayNameExtensions.IsDefined(value); 57 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithSameDisplayNameExtensions.IsDefined(name, allowMatchingMetadataAttribute); 58 | #if READONLYSPAN 59 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => EnumWithSameDisplayNameExtensions.IsDefined(name, allowMatchingMetadataAttribute); 60 | #endif 61 | protected override bool TryParse(string name, out EnumWithSameDisplayName parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumWithSameDisplayNameExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #if READONLYSPAN 64 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithSameDisplayName parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => EnumWithSameDisplayNameExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #endif 67 | protected override bool TryParse(string name, out EnumWithSameDisplayName parsed, EnumParseOptions parseOptions) 68 | => EnumWithSameDisplayNameExtensions.TryParse(name, out parsed, parseOptions); 69 | #if READONLYSPAN 70 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithSameDisplayName parsed, EnumParseOptions parseOptions) 71 | => EnumWithSameDisplayNameExtensions.TryParse(name, out parsed, parseOptions); 72 | #endif 73 | 74 | protected override EnumWithSameDisplayName Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumWithSameDisplayNameExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #if READONLYSPAN 77 | protected override EnumWithSameDisplayName Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => EnumWithSameDisplayNameExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #endif 80 | protected override EnumWithSameDisplayName Parse(string name, EnumParseOptions parseOptions) 81 | => EnumWithSameDisplayNameExtensions.Parse(name, parseOptions); 82 | #if READONLYSPAN 83 | protected override EnumWithSameDisplayName Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 84 | => EnumWithSameDisplayNameExtensions.Parse(name, parseOptions); 85 | #endif 86 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.Interceptors.IntegrationTests/InterceptorTests.cs: -------------------------------------------------------------------------------- 1 | using Foo; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.IO; 5 | using NetEscapades.EnumGenerators; 6 | using Xunit; 7 | 8 | #if NUGET_INTERCEPTOR_TESTS 9 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NETSTANDARD_INTERCEPTOR_TESTS 13 | using NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 14 | 15 | namespace NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests; 16 | #elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests; 18 | #else 19 | #error Unknown project combination 20 | #endif 21 | 22 | public class InterceptorTests 23 | { 24 | [Fact] 25 | public void CallingToStringIsIntercepted() 26 | { 27 | AssertValue(EnumWithDisplayNameInNamespace.First); 28 | AssertValue(EnumWithDisplayNameInNamespace.Second); 29 | AssertValue(EnumWithDisplayNameInNamespace.Third); 30 | 31 | void AssertValue(EnumWithDisplayNameInNamespace value) 32 | { 33 | // This doesn't _actually_ test interception, because can't 34 | // differentiate with built-in version, it's only really verifying the generated code compiles 35 | var toString = value.ToString(); 36 | var fast = value.ToStringFast(); 37 | Assert.Equal(fast, toString); 38 | } 39 | } 40 | 41 | [Fact] 42 | public void CallingToStringIsIntercepted_StringTesting() 43 | { 44 | #pragma warning disable CS0612 45 | var result1 = StringTesting.Backslash.ToString(); 46 | var result2 = StringTesting.Backslash.ToStringFast(); 47 | #pragma warning restore CS0612 48 | Assert.Equal(result1, result2); 49 | } 50 | 51 | [Fact] 52 | public void CallingToStringIsIntercepted_EnumInFoo() 53 | { 54 | // This doesn't _actually_ test interception, because can't 55 | // differentiate with built-in version, it's only really verifying the generated code compiles 56 | var result1 = EnumInFoo.Second.ToString(); 57 | var result2 = EnumInFoo.Second.ToStringFast(); 58 | Assert.Equal(result1, result2); 59 | } 60 | 61 | [Fact] 62 | public void CallingToStringIsIntercepted_EnumWithExtensionInOtherNamespace() 63 | { 64 | // This doesn't _actually_ test interception, because can't 65 | // differentiate with built-in version, it's only really verifying the generated code compiles 66 | var result1 = EnumWithExtensionInOtherNamespace.Second.ToString(); 67 | var result2 = SomethingElse.SomeExtension.ToStringFast(EnumWithExtensionInOtherNamespace.Second); 68 | Assert.Equal(result1, result2); 69 | } 70 | 71 | [Fact] 72 | public void CallingToStringIsIntercepted_ExternalEnum() 73 | { 74 | // This doesn't _actually_ test interception, because can't 75 | // differentiate with built-in version, it's only really verifying the generated code compiles 76 | var result1 = DateTimeKind.Local.ToString(); 77 | var result2 = DateTimeKind.Local.ToStringFast(); 78 | Assert.Equal(result1, result2); 79 | } 80 | 81 | [Fact] 82 | public void CallingHasFlagIsIntercepted() 83 | { 84 | // This doesn't _actually_ test interception, because can't 85 | // differentiate with built-in version, it's only really verifying the generated code compiles 86 | var value1 = FlagsEnum.First; 87 | var result2 = FlagsEnum.Second.HasFlag(value1); 88 | Assert.False(result2); 89 | Assert.True(value1.HasFlag(FlagsEnum.None)); 90 | 91 | var combined = FlagsEnum.First | FlagsEnum.Second; 92 | Assert.True(combined.HasFlag(FlagsEnum.First)); 93 | Assert.False(FlagsEnum.First.HasFlag(combined)); 94 | } 95 | 96 | [Fact] 97 | public void CallingHasFlagIsIntercepted_ExternalEnumFlags() 98 | { 99 | // This doesn't _actually_ test interception, because can't 100 | // differentiate with built-in version, it's only really verifying the generated code compiles 101 | var value1 = FileShare.Read; 102 | var result2 = FileShare.Write.HasFlag(value1); 103 | Assert.False(result2); 104 | Assert.True(value1.HasFlag(FileShare.None)); 105 | 106 | var combined = FileShare.Read | FileShare.Write; 107 | Assert.True(combined.HasFlag(FileShare.Read)); 108 | Assert.False(FileShare.Read.HasFlag(combined)); 109 | } 110 | 111 | [Fact(Skip = "Can't actually verify it's not interceptable")] 112 | public void CallingNonInterceptableEnumIsNotIntercepted() 113 | { 114 | var result1 = NonInterceptableEnum.Second.ToString(); 115 | var result2 = NonInterceptableEnum.Second.ToStringFast(); 116 | Assert.NotEqual(result1, result2); 117 | } 118 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/SourceGenerationHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace NetEscapades.EnumGenerators.Interceptors; 4 | 5 | public static class SourceGenerationHelper 6 | { 7 | private const string Header = 8 | """ 9 | //------------------------------------------------------------------------------ 10 | // 11 | // This code was generated by the NetEscapades.EnumGenerators.Interceptors source generator 12 | // 13 | // Changes to this file may cause incorrect behavior and will be lost if 14 | // the code is regenerated. 15 | // 16 | //------------------------------------------------------------------------------ 17 | 18 | #nullable enable 19 | """; 20 | 21 | public static (string Content, string Filename) GenerateInterceptorsClass(MethodToIntercept toIntercept) 22 | { 23 | var sb = new StringBuilder( 24 | $$""" 25 | {{Header}} 26 | 27 | namespace System.Runtime.CompilerServices 28 | { 29 | // this type is needed by the compiler to implement interceptors - it doesn't need to 30 | // come from the runtime itself, though 31 | 32 | [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate 33 | [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] 34 | sealed file class InterceptsLocationAttribute : global::System.Attribute 35 | { 36 | public InterceptsLocationAttribute(int version, string data) 37 | { 38 | _ = version; 39 | _ = data; 40 | } 41 | } 42 | } 43 | 44 | #pragma warning disable CS0612 // Ignore usages of obsolete members or enums 45 | #pragma warning disable CS0618 // Ignore usages of obsolete members or enums 46 | namespace NetEscapades.EnumGenerators 47 | { 48 | static file class EnumInterceptors 49 | { 50 | 51 | """); 52 | 53 | bool toStringIntercepted = false; 54 | foreach (var location in toIntercept.Invocations) 55 | { 56 | if(location!.Target == InterceptorTarget.ToString) 57 | { 58 | toStringIntercepted = true; 59 | sb.AppendLine(GetInterceptorAttr(location)); 60 | } 61 | } 62 | 63 | if(toStringIntercepted) 64 | { 65 | sb.AppendLine( 66 | $$""" 67 | public static string {{toIntercept.ExtensionTypeName}}ToString(this global::System.Enum value) 68 | => global::{{toIntercept.EnumNamespace}}{{(string.IsNullOrEmpty(toIntercept.EnumNamespace) ? "" : ".")}}{{toIntercept.ExtensionTypeName}}.ToStringFast((global::{{toIntercept.FullyQualifiedName}})value); 69 | 70 | """); 71 | } 72 | 73 | bool hasFlagIntercepted = false; 74 | foreach (var location in toIntercept.Invocations) 75 | { 76 | if(location!.Target == InterceptorTarget.HasFlag) 77 | { 78 | hasFlagIntercepted = true; 79 | sb.AppendLine(GetInterceptorAttr(location)); 80 | } 81 | } 82 | 83 | if(hasFlagIntercepted) 84 | { 85 | sb.AppendLine( 86 | $$""" 87 | public static bool {{toIntercept.ExtensionTypeName}}HasFlag(this global::System.Enum value, global::System.Enum flag) 88 | => global::{{toIntercept.EnumNamespace}}{{(string.IsNullOrEmpty(toIntercept.EnumNamespace) ? "" : ".")}}{{toIntercept.ExtensionTypeName}}.HasFlagFast((global::{{toIntercept.FullyQualifiedName}})value, (global::{{toIntercept.FullyQualifiedName}})flag); 89 | 90 | """); 91 | } 92 | 93 | sb.AppendLine( 94 | """ 95 | } 96 | } 97 | #pragma warning restore CS0612 // Ignore usages of obsolete members or enums 98 | #pragma warning restore CS0618 // Ignore usages of obsolete members or enums 99 | """); 100 | var content = sb.ToString(); 101 | sb.Clear(); 102 | 103 | var filename = sb 104 | .Append(toIntercept.FullyQualifiedName) 105 | .Append("_Interceptors.g.cs") 106 | .Replace('<', '_') 107 | .Replace('>', '_') 108 | .Replace(',', '.') 109 | .Replace(' ', '_') 110 | .ToString(); 111 | return (content, filename); 112 | 113 | static string GetInterceptorAttr(CandidateInvocation location) 114 | { 115 | return $""" [global::System.Runtime.CompilerServices.InterceptsLocation({location.Location.Version}, "{location.Location.Data}")] // {location.Location.GetDisplayLocation()}"""; 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithEnumMemberInNamespaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumWithEnumMemberInNamespaceExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumWithEnumMemberInNamespace.First, 27 | EnumWithEnumMemberInNamespace.Second, 28 | (EnumWithEnumMemberInNamespace)3, 29 | }; 30 | 31 | public TheoryData ValuesToParse() => new() 32 | { 33 | "First", 34 | "Second", 35 | "2nd", 36 | "2ND", 37 | "first", 38 | "SECOND", 39 | "3", 40 | "267", 41 | "-267", 42 | "2147483647", 43 | "3000000000", 44 | "Fourth", 45 | "Fifth", 46 | }; 47 | 48 | protected override string[] GetNames() => EnumWithEnumMemberInNamespaceExtensions.GetNames(); 49 | protected override EnumWithEnumMemberInNamespace[] GetValues() => EnumWithEnumMemberInNamespaceExtensions.GetValues(); 50 | protected override int[] GetValuesAsUnderlyingType() => EnumWithEnumMemberInNamespaceExtensions.GetValuesAsUnderlyingType(); 51 | protected override int AsUnderlyingValue(EnumWithEnumMemberInNamespace value) => value.AsUnderlyingType(); 52 | 53 | protected override string ToStringFast(EnumWithEnumMemberInNamespace value) => value.ToStringFast(); 54 | protected override string ToStringFast(EnumWithEnumMemberInNamespace value, bool withMetadata) => value.ToStringFast(withMetadata); 55 | protected override string ToStringFast(EnumWithEnumMemberInNamespace value, SerializationOptions options) => value.ToStringFast(options); 56 | protected override bool IsDefined(EnumWithEnumMemberInNamespace value) => EnumWithEnumMemberInNamespaceExtensions.IsDefined(value); 57 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithEnumMemberInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 58 | #if READONLYSPAN 59 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute = false) => EnumWithEnumMemberInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 60 | #endif 61 | protected override bool TryParse(string name, out EnumWithEnumMemberInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumWithEnumMemberInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #if READONLYSPAN 64 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithEnumMemberInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => EnumWithEnumMemberInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #endif 67 | protected override bool TryParse(string name, out EnumWithEnumMemberInNamespace parsed, EnumParseOptions parseOptions) 68 | => EnumWithEnumMemberInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 69 | #if READONLYSPAN 70 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithEnumMemberInNamespace parsed, EnumParseOptions parseOptions) 71 | => EnumWithEnumMemberInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 72 | #endif 73 | 74 | protected override EnumWithEnumMemberInNamespace Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumWithEnumMemberInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #if READONLYSPAN 77 | protected override EnumWithEnumMemberInNamespace Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => EnumWithEnumMemberInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #endif 80 | protected override EnumWithEnumMemberInNamespace Parse(string name, EnumParseOptions parseOptions) 81 | => EnumWithEnumMemberInNamespaceExtensions.Parse(name, parseOptions); 82 | #if READONLYSPAN 83 | protected override EnumWithEnumMemberInNamespace Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 84 | => EnumWithEnumMemberInNamespaceExtensions.Parse(name, parseOptions); 85 | #endif 86 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithDescriptionInNamespaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumWithDescriptionInNamespaceExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumWithDescriptionInNamespace.First, 27 | EnumWithDescriptionInNamespace.Second, 28 | (EnumWithDescriptionInNamespace)3, 29 | }; 30 | 31 | public TheoryData ValuesToParse() => new() 32 | { 33 | "First", 34 | "Second", 35 | "2nd", 36 | "2ND", 37 | "first", 38 | "SECOND", 39 | "3", 40 | "267", 41 | "-267", 42 | "2147483647", 43 | "3000000000", 44 | "Fourth", 45 | "Fifth", 46 | }; 47 | 48 | protected override string[] GetNames() => EnumWithDescriptionInNamespaceExtensions.GetNames(); 49 | protected override EnumWithDescriptionInNamespace[] GetValues() => EnumWithDescriptionInNamespaceExtensions.GetValues(); 50 | protected override int[] GetValuesAsUnderlyingType() => EnumWithDescriptionInNamespaceExtensions.GetValuesAsUnderlyingType(); 51 | protected override int AsUnderlyingValue(EnumWithDescriptionInNamespace value) => value.AsUnderlyingType(); 52 | 53 | protected override string ToStringFast(EnumWithDescriptionInNamespace value) => value.ToStringFast(); 54 | protected override string ToStringFast(EnumWithDescriptionInNamespace value, bool withMetadata) => value.ToStringFast(withMetadata); 55 | protected override string ToStringFast(EnumWithDescriptionInNamespace value, SerializationOptions options) => value.ToStringFast(options); 56 | protected override bool IsDefined(EnumWithDescriptionInNamespace value) => EnumWithDescriptionInNamespaceExtensions.IsDefined(value); 57 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithDescriptionInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 58 | #if READONLYSPAN 59 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute = false) => EnumWithDescriptionInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 60 | #endif 61 | protected override bool TryParse(string name, out EnumWithDescriptionInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumWithDescriptionInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #if READONLYSPAN 64 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithDescriptionInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => EnumWithDescriptionInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #endif 67 | protected override bool TryParse(string name, out EnumWithDescriptionInNamespace parsed, EnumParseOptions parseOptions) 68 | => EnumWithDescriptionInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 69 | #if READONLYSPAN 70 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithDescriptionInNamespace parsed, EnumParseOptions parseOptions) 71 | => EnumWithDescriptionInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 72 | #endif 73 | 74 | protected override EnumWithDescriptionInNamespace Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumWithDescriptionInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #if READONLYSPAN 77 | protected override EnumWithDescriptionInNamespace Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => EnumWithDescriptionInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #endif 80 | protected override EnumWithDescriptionInNamespace Parse(string name, EnumParseOptions parseOptions) 81 | => EnumWithDescriptionInNamespaceExtensions.Parse(name, parseOptions); 82 | #if READONLYSPAN 83 | protected override EnumWithDescriptionInNamespace Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 84 | => EnumWithDescriptionInNamespaceExtensions.Parse(name, parseOptions); 85 | #endif 86 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithDisplayNameInNamespaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | public class EnumWithDisplayNameInNamespaceExtensionsTests : ExtensionTests, ITestData 23 | { 24 | public TheoryData ValidEnumValues() => new() 25 | { 26 | EnumWithDisplayNameInNamespace.First, 27 | EnumWithDisplayNameInNamespace.Second, 28 | (EnumWithDisplayNameInNamespace)3, 29 | }; 30 | 31 | public TheoryData ValuesToParse() => new() 32 | { 33 | "First", 34 | "Second", 35 | "2nd", 36 | "2ND", 37 | "first", 38 | "SECOND", 39 | "3", 40 | "267", 41 | "-267", 42 | "2147483647", 43 | "3000000000", 44 | "Fourth", 45 | "Fifth", 46 | }; 47 | 48 | protected override string[] GetNames() => EnumWithDisplayNameInNamespaceExtensions.GetNames(); 49 | protected override EnumWithDisplayNameInNamespace[] GetValues() => EnumWithDisplayNameInNamespaceExtensions.GetValues(); 50 | protected override int[] GetValuesAsUnderlyingType() => EnumWithDisplayNameInNamespaceExtensions.GetValuesAsUnderlyingType(); 51 | protected override int AsUnderlyingValue(EnumWithDisplayNameInNamespace value) => value.AsUnderlyingType(); 52 | 53 | protected override string ToStringFast(EnumWithDisplayNameInNamespace value) => value.ToStringFast(); 54 | protected override string ToStringFast(EnumWithDisplayNameInNamespace value, bool withMetadata) => value.ToStringFast(withMetadata); 55 | protected override string ToStringFast(EnumWithDisplayNameInNamespace value, SerializationOptions options) => value.ToStringFast(options); 56 | protected override bool IsDefined(EnumWithDisplayNameInNamespace value) => EnumWithDisplayNameInNamespaceExtensions.IsDefined(value); 57 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithDisplayNameInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 58 | #if READONLYSPAN 59 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute = false) => EnumWithDisplayNameInNamespaceExtensions.IsDefined(name, allowMatchingMetadataAttribute); 60 | #endif 61 | protected override bool TryParse(string name, out EnumWithDisplayNameInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 62 | => EnumWithDisplayNameInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 63 | #if READONLYSPAN 64 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithDisplayNameInNamespace parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 65 | => EnumWithDisplayNameInNamespaceExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 66 | #endif 67 | protected override bool TryParse(string name, out EnumWithDisplayNameInNamespace parsed, EnumParseOptions parseOptions) 68 | => EnumWithDisplayNameInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 69 | #if READONLYSPAN 70 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithDisplayNameInNamespace parsed, EnumParseOptions parseOptions) 71 | => EnumWithDisplayNameInNamespaceExtensions.TryParse(name, out parsed, parseOptions); 72 | #endif 73 | 74 | protected override EnumWithDisplayNameInNamespace Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 75 | => EnumWithDisplayNameInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 76 | #if READONLYSPAN 77 | protected override EnumWithDisplayNameInNamespace Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 78 | => EnumWithDisplayNameInNamespaceExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 79 | #endif 80 | protected override EnumWithDisplayNameInNamespace Parse(string name, EnumParseOptions parseOptions) 81 | => EnumWithDisplayNameInNamespaceExtensions.Parse(name, parseOptions); 82 | #if READONLYSPAN 83 | protected override EnumWithDisplayNameInNamespace Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 84 | => EnumWithDisplayNameInNamespaceExtensions.Parse(name, parseOptions); 85 | #endif 86 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | .idea 6 | *.suo 7 | *.user 8 | *.userosscache 9 | *.sln.docstates 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | [Xx]64/ 20 | [Xx]86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ 246 | tools/* 247 | !tools/packages.config 248 | 249 | 250 | *.received.txt 251 | BenchmarkDotNet.Artifacts 252 | 253 | # Nuke.build files 254 | .nuke/build.schema.json -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/EnumWithNoMetadataSourcesInNamespaceExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | #if INTEGRATION_TESTS 5 | namespace NetEscapades.EnumGenerators.IntegrationTests; 6 | #elif NETSTANDARD_INTEGRATION_TESTS 7 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 8 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 10 | #elif INTERCEPTOR_TESTS 11 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 12 | #elif NUGET_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 14 | #elif NUGET_INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 16 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 18 | #else 19 | #error Unknown integration tests 20 | #endif 21 | 22 | #nullable enable 23 | public class EnumWithNoMetadataSourcesInNamespaceExtensionsTests : ExtensionTests, ITestData 24 | { 25 | public TheoryData ValidEnumValues() => new() 26 | { 27 | EnumWithNoMetadataSources.First, 28 | EnumWithNoMetadataSources.Second, 29 | (EnumWithNoMetadataSources)3, 30 | }; 31 | 32 | public TheoryData ValuesToParse() => new() 33 | { 34 | "First", 35 | "Second", 36 | "2nd", 37 | "2ND", 38 | "first", 39 | "SECOND", 40 | "3", 41 | "267", 42 | "-267", 43 | "2147483647", 44 | "3000000000", 45 | "Fourth", 46 | "Fifth", 47 | }; 48 | 49 | protected override string[] GetNames() => EnumWithNoMetadataSourcesExtensions.GetNames(); 50 | protected override EnumWithNoMetadataSources[] GetValues() => EnumWithNoMetadataSourcesExtensions.GetValues(); 51 | protected override int[] GetValuesAsUnderlyingType() => EnumWithNoMetadataSourcesExtensions.GetValuesAsUnderlyingType(); 52 | protected override int AsUnderlyingValue(EnumWithNoMetadataSources value) => value.AsUnderlyingType(); 53 | 54 | // Can't call the "withMetadata" versions of all these 55 | protected override string ToStringFast(EnumWithNoMetadataSources value) => value.ToStringFast(); 56 | protected override string ToStringFast(EnumWithNoMetadataSources value, bool withMetadata) => value.ToStringFast(); 57 | protected override string ToStringFast(EnumWithNoMetadataSources value, SerializationOptions options) => value.ToStringFast(options); 58 | protected override bool IsDefined(EnumWithNoMetadataSources value) => EnumWithNoMetadataSourcesExtensions.IsDefined(value); 59 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => EnumWithNoMetadataSourcesExtensions.IsDefined(name); 60 | #if READONLYSPAN 61 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute = false) => EnumWithNoMetadataSourcesExtensions.IsDefined(name); 62 | #endif 63 | protected override bool TryParse(string name, out EnumWithNoMetadataSources parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 64 | => EnumWithNoMetadataSourcesExtensions.TryParse(name, out parsed, ignoreCase); 65 | #if READONLYSPAN 66 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithNoMetadataSources parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 67 | => EnumWithNoMetadataSourcesExtensions.TryParse(name, out parsed, ignoreCase); 68 | #endif 69 | protected override bool TryParse(string name, out EnumWithNoMetadataSources parsed, EnumParseOptions parseOptions) 70 | => EnumWithNoMetadataSourcesExtensions.TryParse(name, out parsed, parseOptions); 71 | #if READONLYSPAN 72 | protected override bool TryParse(in ReadOnlySpan name, out EnumWithNoMetadataSources parsed, EnumParseOptions parseOptions) 73 | => EnumWithNoMetadataSourcesExtensions.TryParse(name, out parsed, parseOptions); 74 | #endif 75 | 76 | protected override EnumWithNoMetadataSources Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 77 | => EnumWithNoMetadataSourcesExtensions.Parse(name, ignoreCase); 78 | #if READONLYSPAN 79 | protected override EnumWithNoMetadataSources Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 80 | => EnumWithNoMetadataSourcesExtensions.Parse(name, ignoreCase); 81 | #endif 82 | protected override EnumWithNoMetadataSources Parse(string name, EnumParseOptions parseOptions) 83 | => EnumWithNoMetadataSourcesExtensions.Parse(name, parseOptions); 84 | #if READONLYSPAN 85 | protected override EnumWithNoMetadataSources Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 86 | => EnumWithNoMetadataSourcesExtensions.Parse(name, parseOptions); 87 | #endif 88 | 89 | protected override bool TryGetDisplayNameOrDescription( 90 | string? value, 91 | #if NETCOREAPP3_0_OR_GREATER 92 | [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string? displayName) 93 | #else 94 | out string? displayName) 95 | #endif 96 | { 97 | // no metadata 98 | displayName = null; 99 | return false; 100 | } 101 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/FlagsEnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | #if INTEGRATION_TESTS 8 | namespace NetEscapades.EnumGenerators.IntegrationTests; 9 | #elif NETSTANDARD_INTEGRATION_TESTS 10 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 11 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 12 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 13 | #elif INTERCEPTOR_TESTS 14 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 15 | #elif NUGET_INTEGRATION_TESTS 16 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 17 | #elif NUGET_INTERCEPTOR_TESTS 18 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 19 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 20 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 21 | #else 22 | #error Unknown integration tests 23 | #endif 24 | 25 | 26 | #pragma warning disable NEEG001 27 | #pragma warning disable NEEG002 28 | #pragma warning disable NEEG003 29 | #pragma warning disable NEEG004 30 | #pragma warning disable NEEG005 31 | 32 | public class FlagsEnumExtensionsTests : ExtensionTests, ITestData 33 | { 34 | public TheoryData ValidEnumValues() => new() 35 | { 36 | FlagsEnum.First, 37 | FlagsEnum.Second, 38 | FlagsEnum.ThirdAndFourth, 39 | (FlagsEnum)3, 40 | }; 41 | 42 | public TheoryData ValuesToParse() => new() 43 | { 44 | "First", 45 | "Second", 46 | "2nd", 47 | "2ND", 48 | "first", 49 | "SECOND", 50 | "3", 51 | "267", 52 | "-267", 53 | "2147483647", 54 | "3000000000", 55 | "Fourth", 56 | "Fifth", 57 | }; 58 | 59 | protected override string[] GetNames() => FlagsEnumExtensions.GetNames(); 60 | protected override FlagsEnum[] GetValues() => FlagsEnumExtensions.GetValues(); 61 | protected override int[] GetValuesAsUnderlyingType() => FlagsEnumExtensions.GetValuesAsUnderlyingType(); 62 | protected override int AsUnderlyingValue(FlagsEnum value) => value.AsUnderlyingType(); 63 | 64 | protected override string ToStringFast(FlagsEnum value) => value.ToStringFast(); 65 | protected override string ToStringFast(FlagsEnum value, bool withMetadata) => value.ToStringFast(withMetadata); 66 | protected override string ToStringFast(FlagsEnum value, SerializationOptions options) => value.ToStringFast(options); 67 | protected override bool IsDefined(FlagsEnum value) => FlagsEnumExtensions.IsDefined(value); 68 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => FlagsEnumExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 69 | #if READONLYSPAN 70 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => FlagsEnumExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 71 | #endif 72 | protected override bool TryParse(string name, out FlagsEnum parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 73 | => FlagsEnumExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 74 | #if READONLYSPAN 75 | protected override bool TryParse(in ReadOnlySpan name, out FlagsEnum parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 76 | => FlagsEnumExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 77 | #endif 78 | protected override bool TryParse(string name, out FlagsEnum parsed, EnumParseOptions parseOptions) 79 | => FlagsEnumExtensions.TryParse(name, out parsed, parseOptions); 80 | #if READONLYSPAN 81 | protected override bool TryParse(in ReadOnlySpan name, out FlagsEnum parsed, EnumParseOptions parseOptions) 82 | => FlagsEnumExtensions.TryParse(name, out parsed, parseOptions); 83 | #endif 84 | 85 | protected override FlagsEnum Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 86 | => FlagsEnumExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 87 | #if READONLYSPAN 88 | protected override FlagsEnum Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 89 | => FlagsEnumExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 90 | #endif 91 | protected override FlagsEnum Parse(string name, EnumParseOptions parseOptions) 92 | => FlagsEnumExtensions.Parse(name, parseOptions); 93 | #if READONLYSPAN 94 | protected override FlagsEnum Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 95 | => FlagsEnumExtensions.Parse(name, parseOptions); 96 | #endif 97 | 98 | public static IEnumerable AllFlags() 99 | { 100 | var values = new[] 101 | { 102 | FlagsEnum.First, 103 | FlagsEnum.Second, 104 | FlagsEnum.Third, 105 | FlagsEnum.ThirdAndFourth, 106 | FlagsEnum.First | FlagsEnum.Second, 107 | (FlagsEnum)65, 108 | (FlagsEnum)0, 109 | }; 110 | 111 | return from v1 in values 112 | from v2 in values 113 | select new object[] { v1, v2 }; 114 | } 115 | 116 | [Theory] 117 | [MemberData(nameof(AllFlags))] 118 | public void HasFlags(FlagsEnum value, FlagsEnum flag) 119 | { 120 | var isDefined = value.HasFlagFast(flag); 121 | 122 | isDefined.Should().Be(value.HasFlag(flag)); 123 | } 124 | } -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/ExternalFlagsEnumExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | #if INTEGRATION_TESTS 9 | namespace NetEscapades.EnumGenerators.IntegrationTests; 10 | #elif NETSTANDARD_INTEGRATION_TESTS 11 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests; 12 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 13 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests; 14 | #elif INTERCEPTOR_TESTS 15 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests; 16 | #elif NUGET_INTEGRATION_TESTS 17 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests; 18 | #elif NUGET_INTERCEPTOR_TESTS 19 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests; 20 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 21 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests; 22 | #else 23 | #error Unknown integration tests 24 | #endif 25 | 26 | #pragma warning disable NEEG001 27 | #pragma warning disable NEEG002 28 | #pragma warning disable NEEG003 29 | #pragma warning disable NEEG004 30 | #pragma warning disable NEEG005 31 | 32 | public class ExternalFileShareExtensionsTests : ExtensionTests, ITestData 33 | { 34 | public TheoryData ValidEnumValues() => new() 35 | { 36 | FileShare.Read, 37 | FileShare.Write, 38 | FileShare.Delete | FileShare.Read, 39 | (FileShare)3, 40 | }; 41 | 42 | public TheoryData ValuesToParse() => new() 43 | { 44 | "Read", 45 | "Write", 46 | "BEEP", 47 | "Boop", 48 | "read", 49 | "WRITE", 50 | "3", 51 | "267", 52 | "-267", 53 | "2147483647", 54 | "3000000000", 55 | "Fourth", 56 | "Fifth", 57 | }; 58 | 59 | protected override string[] GetNames() => FileShareExtensions.GetNames(); 60 | protected override FileShare[] GetValues() => FileShareExtensions.GetValues(); 61 | protected override int[] GetValuesAsUnderlyingType() => FileShareExtensions.GetValuesAsUnderlyingType(); 62 | protected override int AsUnderlyingValue(FileShare value) => value.AsUnderlyingType(); 63 | 64 | protected override string ToStringFast(FileShare value) => value.ToStringFast(); 65 | protected override string ToStringFast(FileShare value, bool withMetadata) => value.ToStringFast(withMetadata); 66 | protected override string ToStringFast(FileShare value, SerializationOptions options) => value.ToStringFast(options); 67 | protected override bool IsDefined(FileShare value) => FileShareExtensions.IsDefined(value); 68 | protected override bool IsDefined(string name, bool allowMatchingMetadataAttribute) => FileShareExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 69 | #if READONLYSPAN 70 | protected override bool IsDefined(in ReadOnlySpan name, bool allowMatchingMetadataAttribute) => FileShareExtensions.IsDefined(name, allowMatchingMetadataAttribute: false); 71 | #endif 72 | protected override bool TryParse(string name, out FileShare parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 73 | => FileShareExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 74 | #if READONLYSPAN 75 | protected override bool TryParse(in ReadOnlySpan name, out FileShare parsed, bool ignoreCase, bool allowMatchingMetadataAttribute) 76 | => FileShareExtensions.TryParse(name, out parsed, ignoreCase, allowMatchingMetadataAttribute); 77 | #endif 78 | protected override bool TryParse(string name, out FileShare parsed, EnumParseOptions parseOptions) 79 | => FileShareExtensions.TryParse(name, out parsed, parseOptions); 80 | #if READONLYSPAN 81 | protected override bool TryParse(in ReadOnlySpan name, out FileShare parsed, EnumParseOptions parseOptions) 82 | => FileShareExtensions.TryParse(name, out parsed, parseOptions); 83 | #endif 84 | 85 | protected override FileShare Parse(string name, bool ignoreCase, bool allowMatchingMetadataAttribute) 86 | => FileShareExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 87 | #if READONLYSPAN 88 | protected override FileShare Parse(in ReadOnlySpan name, bool ignoreCase, bool allowMatchingMetadataAttribute) 89 | => FileShareExtensions.Parse(name, ignoreCase, allowMatchingMetadataAttribute); 90 | #endif 91 | protected override FileShare Parse(string name, EnumParseOptions parseOptions) 92 | => FileShareExtensions.Parse(name, parseOptions); 93 | #if READONLYSPAN 94 | protected override FileShare Parse(in ReadOnlySpan name, EnumParseOptions parseOptions) 95 | => FileShareExtensions.Parse(name, parseOptions); 96 | #endif 97 | 98 | 99 | public static IEnumerable AllFlags() 100 | { 101 | var values = new[] 102 | { 103 | FileShare.Read, 104 | FileShare.Write, 105 | FileShare.ReadWrite, 106 | FileShare.Inheritable, 107 | FileShare.Delete | FileShare.Read, 108 | (FileShare)65, 109 | (FileShare)0, 110 | }; 111 | 112 | return from v1 in values 113 | from v2 in values 114 | select new object[] { v1, v2 }; 115 | } 116 | 117 | [Theory] 118 | [MemberData(nameof(AllFlags))] 119 | public void HasFlags(FileShare value, FileShare flag) 120 | { 121 | var isDefined = value.HasFlagFast(flag); 122 | 123 | isDefined.Should().Be(value.HasFlag(flag)); 124 | } 125 | } -------------------------------------------------------------------------------- /src/NetEscapades.EnumGenerators.Interceptors/README.md: -------------------------------------------------------------------------------- 1 | # ![](https://raw.githubusercontent.com/andrewlock/NetEscapades.EnumGenerators/refs/heads/main/icon_32.png) NetEscapades.EnumGenerators.Interceptors 2 | 3 | A source generator interceptor for automatically intercepting calls to `ToString()` on enums, and replacing them with calls to `ToStringFast()` generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) 4 | 5 | > This source generator requires the .NET 8.0.400 SDK. You can target earlier frameworks like .NET Core 3.1 etc, but the _SDK_ must be at least 8.0.400 6 | 7 | ## Why use this package? 8 | 9 | Many methods that operate with enums, such as the `ToString()` or `HasFlag()` method, are surprisingly slow. The [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) uses a source generator to provide _fast_ versions of these methods, such as `ToStringFast()` or `HasFlagFast()`. 10 | 11 | The main downside to the extension methods generated by [NetEscapades.EnumGenerators](https://www.nuget.org/packages/NetEscapades.EnumGenerators) is that you have to remember to use them. The [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) package solves this problem by intercepting calls to `ToString()` and replacing them with calls `ToStringFast()` automatically using the .NET compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). 12 | 13 | > Interceptors were introduced as an experimental feature in C#12 with .NET 8, and are stable in .NET 9. They allow a source generator to "intercept" certain method calls, and replace the call with a different one. 14 | 15 | For example if you have this code: 16 | 17 | ```csharp 18 | var choice = Color.Red; 19 | Console.WriteLine("You chose: " + choice.ToString()); 20 | 21 | public enum Color 22 | { 23 | Red = 0, 24 | Blue = 1, 25 | } 26 | ``` 27 | 28 | When you use the [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors), the interceptor automatically replaces the call to `choice.ToString()` at compile-time with a call to `ToStringFast()`, as though you wrote the following: 29 | 30 | ```csharp 31 | // The compiler replaces the call with this 👇 32 | Console.WriteLine("You chose: " + choice.ToStringFast()); 33 | ``` 34 | 35 | There are many caveats to this behaviour, as described below, but any explicit calls to `ToString()` or `HasFlag()` on a supported enum are replaced automatically. 36 | 37 | ## Adding NetEscapades.EnumGenerators.Interceptors to your project 38 | 39 | Add the package to your application using: 40 | 41 | ```bash 42 | dotnet add package NetEscapades.EnumGenerators.Interceptors 43 | ``` 44 | 45 | This adds a `` to your project. You can additionally mark the package as `PrivateAssets="all"` and `ExcludeAssets="runtime"`. 46 | 47 | > Setting `PrivateAssets="all"` means any projects referencing this one won't get a reference to the _NetEscapades.EnumGenerators.Interceptors_ package. Setting `ExcludeAssets="runtime"` ensures the _NetEscapades.EnumGenerators.Interceptors.Attributes.dll_ file is _not_ copied to your build output (it is not required at runtime). 48 | 49 | ```xml 50 | 51 | 52 | 53 | Exe 54 | net8.0 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | ``` 64 | 65 | ## Enabling interception for an enum 66 | 67 | By default, adding [NetEscapades.EnumGenerators.Interceptors](https://www.nuget.org/packages/NetEscapades.EnumGenerators.Interceptors) to a project enables interception for all enums _defined in that project_ that use the `[EnumExtensions]` or `[EnumExtensions]` attributes. If you wish to intercept calls made to enums with extensions defined in _other_ projects, you must add the `[Interceptable]` attribute in the project where you want the interception to happen, e.g. 68 | 69 | ```csharp 70 | [assembly:Interceptable] 71 | [assembly:Interceptable] 72 | ``` 73 | 74 | If you don't want a specific enum to be intercepted, you can set the `IsInterceptable` property to `false`, e.g. 75 | 76 | ```csharp 77 | [EnumExtensions(IsInterceptable = false)] 78 | public enum Colour 79 | { 80 | Red = 0, 81 | Blue = 1, 82 | } 83 | ``` 84 | 85 | Interception only works when the target type is unambiguously an interceptable enum, so it won't work 86 | 87 | - When `ToString()` is called in other source generated code. 88 | - When `ToString()` is called in already-compiled code. 89 | - If the `ToString()` call is _implicit_ (for example in `string` interpolation) 90 | - If the `ToString()` call is made on a base type, such as `System.Enum` or `object` 91 | - If the `ToString()` call is made on a generic type 92 | 93 | For example: 94 | 95 | ```csharp 96 | // All the examples in this method CAN be intercepted 97 | public void CanIntercept() 98 | { 99 | var ok1 = Color.Red.ToString(); // ✅ 100 | var red = Color.Red; 101 | var ok2 = red.ToString(); // ✅ 102 | var ok3 = "The colour is " + red.ToString(); // ✅ 103 | var ok4 = $"The colour is {red.ToString()}"; // ✅ 104 | } 105 | 106 | // The examples in this method can NOT be intercepted 107 | public void CantIntercept() 108 | { 109 | var bad1 = ((System.Enum)Color.Red).ToString(); // ❌ Base type 110 | var bad2 = ((object)Color.Red).ToString(); // ❌ Base type 111 | 112 | var bad3 = "The colour is " + red; // ❌ implicit 113 | var bad4 = $"The colour is {red}"; // ❌ implicit 114 | 115 | string Write(T val) 116 | where T : Enum 117 | { 118 | return val.ToString(); // ❌ generic 119 | } 120 | } 121 | ``` -------------------------------------------------------------------------------- /tests/NetEscapades.EnumGenerators.IntegrationTests/Enums.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Runtime.Serialization; 5 | 6 | #pragma warning disable NEEG003 7 | 8 | [assembly:NetEscapades.EnumGenerators.EnumExtensions()] 9 | [assembly:NetEscapades.EnumGenerators.EnumExtensions()] 10 | 11 | namespace System 12 | { 13 | using NetEscapades.EnumGenerators; 14 | 15 | [EnumExtensions] 16 | public enum EnumInSystem 17 | { 18 | First = 0, 19 | Second = 1, 20 | Third = 2, 21 | } 22 | 23 | [EnumExtensions(IsInterceptable = false)] 24 | public enum NonInterceptableEnum 25 | { 26 | First = 0, 27 | [EnumMember(Value = "2nd")] Second = 1, 28 | Third = 2, 29 | } 30 | } 31 | 32 | namespace Foo 33 | { 34 | using NetEscapades.EnumGenerators; 35 | 36 | // causes Error CS0426 : The type name 'TestEnum' does not exist in the type 'Foo'. 37 | // workaround is to use global prefix 38 | 39 | public class Foo 40 | { 41 | } 42 | 43 | [EnumExtensions] 44 | public enum EnumInFoo 45 | { 46 | First = 0, 47 | [EnumMember(Value = "2nd")] 48 | Second = 1, 49 | Third = 2, 50 | } 51 | } 52 | 53 | 54 | #if INTEGRATION_TESTS 55 | namespace NetEscapades.EnumGenerators.IntegrationTests 56 | #elif NETSTANDARD_INTEGRATION_TESTS 57 | namespace NetEscapades.EnumGenerators.NetStandard.IntegrationTests 58 | #elif NETSTANDARD_SYSTEMMEMORY_INTEGRATION_TESTS 59 | namespace NetEscapades.EnumGenerators.NetStandard.SystemMemory.IntegrationTests 60 | #elif INTERCEPTOR_TESTS 61 | namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests 62 | #elif NUGET_INTEGRATION_TESTS 63 | namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests 64 | #elif NUGET_INTERCEPTOR_TESTS 65 | namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests 66 | #elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS 67 | namespace NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests 68 | #elif NUGET_SYSTEMMEMORY_INTEGRATION_TESTS 69 | namespace NetEscapades.EnumGenerators.Nuget.SystemMemory.IntegrationTests 70 | #else 71 | #error Unknown integration tests 72 | #endif 73 | { 74 | [EnumExtensions] 75 | public enum EnumInNamespace 76 | { 77 | First = 0, 78 | Second = 1, 79 | Third = 2, 80 | } 81 | 82 | [EnumExtensions(MetadataSource = MetadataSource.DisplayAttribute)] 83 | public enum EnumWithDisplayNameInNamespace 84 | { 85 | First = 0, 86 | 87 | [Display(Name = "2nd")] 88 | Second = 1, 89 | 90 | Third = 2, 91 | } 92 | 93 | [EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)] 94 | public enum EnumWithDescriptionInNamespace 95 | { 96 | First = 0, 97 | 98 | [Description("2nd")] 99 | Second = 1, 100 | 101 | Third = 2, 102 | } 103 | 104 | [EnumExtensions(MetadataSource = MetadataSource.EnumMemberAttribute)] 105 | public enum EnumWithEnumMemberInNamespace 106 | { 107 | First = 0, 108 | 109 | [EnumMember(Value = "2nd")] 110 | Second = 1, 111 | 112 | Third = 2, 113 | } 114 | 115 | [EnumExtensions(MetadataSource = MetadataSource.None)] 116 | public enum EnumWithNoMetadataSources 117 | { 118 | First = 0, 119 | 120 | [EnumMember(Value = "2nd")] 121 | Second = 1, 122 | 123 | Third = 2, 124 | } 125 | 126 | [EnumExtensions(MetadataSource = MetadataSource.DisplayAttribute)] 127 | public enum EnumWithSameDisplayName 128 | { 129 | First = 0, 130 | 131 | [Display(Name = "2nd")] 132 | Second = 1, 133 | 134 | [Display(Name = "2nd")] 135 | Third = 2, 136 | } 137 | 138 | [EnumExtensions] 139 | public enum LongEnum: long 140 | { 141 | Second = 1, 142 | First = 0, 143 | Third = 2, 144 | } 145 | 146 | [EnumExtensions] 147 | [Flags] 148 | public enum FlagsEnum 149 | { 150 | None = 0, 151 | Second = 1 << 1, 152 | First = 1 << 0, 153 | Third = 1 << 2, 154 | Fourth = 1 << 3, 155 | ThirdAndFourth = Third | Fourth, 156 | } 157 | 158 | [EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)] 159 | public enum StringTesting 160 | { 161 | [System.ComponentModel.Description("Quotes \"")] Quotes, 162 | [System.ComponentModel.Description(@"Literal Quotes """)] LiteralQuotes, 163 | [Obsolete] 164 | [System.ComponentModel.Description("Backslash \\")] Backslash, 165 | [System.ComponentModel.Description(@"LiteralBackslash \")] BackslashLiteral, 166 | [System.ComponentModel.Description("Line\nBreak")] LineBreak, 167 | } 168 | 169 | [EnumExtensions(ExtensionClassName="SomeExtension", ExtensionClassNamespace = "SomethingElse")] 170 | public enum EnumWithExtensionInOtherNamespace 171 | { 172 | First = 0, 173 | 174 | [EnumMember(Value = "2nd")] Second = 1, 175 | 176 | Third = 2, 177 | } 178 | 179 | [EnumExtensions] 180 | public enum EnumWithRepeatedValues 181 | { 182 | First = 0, 183 | Second = 1, 184 | Third = 0, // Repeated value 185 | Fourth = Second, // Repeated value 186 | Fifth = Second + Third, // Repeated value 187 | } 188 | 189 | [EnumExtensions] 190 | public enum EnumWithRepeatedValuesWithDisplayNames 191 | { 192 | [EnumMember(Value = "Main")] First = 0, 193 | Second = 1, 194 | [EnumMember(Value = "Repeated")] Third = 0, // Repeated value with display name 195 | } 196 | 197 | [EnumExtensions(MetadataSource = MetadataSource.DescriptionAttribute)] 198 | public enum EnumWithReservedKeywords 199 | { 200 | [Description("number")] 201 | number, 202 | [Description("string")] 203 | @string, 204 | [Description("date")] 205 | date, 206 | [Description("class")] 207 | @class, 208 | } 209 | } --------------------------------------------------------------------------------