From a38e5ad5efbb628641f068299e7cd47473251302 Mon Sep 17 00:00:00 2001 From: "ash.blade" Date: Fri, 9 Jun 2023 19:17:45 +0300 Subject: [PATCH 1/5] Add tests for generating enum class for enum from external assemblies --- EnumClass.sln | 10 + .../EnumClass.RawEnumComparison/Program.cs | 3 + .../ExternalEnumClassAttribute.cs | 30 ++ src/EnumClass.Core/EnumInfoFactory.cs | 88 +++- .../ExternalEnumClassAttributeInfo.cs | 9 + .../Infrastructure/Constants.cs | 15 +- .../EnumClass.Generator.csproj | 2 +- .../EnumClassIncrementalGenerator.cs | 495 +++++------------- src/EnumClass.Generator/GenerationContext.cs | 21 + src/EnumClass.Generator/GeneratorHelpers.cs | 352 +++++++++++++ .../Infrastructure/NameSyntaxExtensions.cs | 17 + .../SeparatedSyntaxListExtensions.cs | 32 ++ .../Infrastructure/SyntaxListExtensions.cs | 27 + ...umClass.Generator.Tests.Integration.csproj | 1 + .../ExternalEnumClassGeneratorTests.cs | 31 ++ .../SwitchTests.cs | 28 + .../EnumClass.Generator.Tests.csproj | 1 + .../ExternalEnumClassAttributeTests.cs | 38 ++ .../Generator/SampleEnums/SampleEnums.csproj | 9 + tests/Generator/SampleEnums/Token.cs | 9 + 20 files changed, 842 insertions(+), 376 deletions(-) create mode 100644 src/EnumClass.Attributes/ExternalEnumClassAttribute.cs create mode 100644 src/EnumClass.Core/ExternalEnumClassAttributeInfo.cs create mode 100644 src/EnumClass.Generator/GenerationContext.cs create mode 100644 src/EnumClass.Generator/GeneratorHelpers.cs create mode 100644 src/EnumClass.Generator/Infrastructure/NameSyntaxExtensions.cs create mode 100644 src/EnumClass.Generator/Infrastructure/SeparatedSyntaxListExtensions.cs create mode 100644 src/EnumClass.Generator/Infrastructure/SyntaxListExtensions.cs create mode 100644 tests/Generator/EnumClass.Generator.Tests.Integration/ExternalEnumClassGeneratorTests.cs create mode 100644 tests/Generator/EnumClass.Generator.Tests/ExternalEnumClassAttributeTests.cs create mode 100644 tests/Generator/SampleEnums/SampleEnums.csproj create mode 100644 tests/Generator/SampleEnums/Token.cs diff --git a/EnumClass.sln b/EnumClass.sln index 91e2896..828897f 100644 --- a/EnumClass.sln +++ b/EnumClass.sln @@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumClass.JsonConverter.Gen EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumClass.JsonSerialization", "samples\EnumClass.JsonSerialization\EnumClass.JsonSerialization.csproj", "{7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HelperProjects", "HelperProjects", "{52329321-E2A3-468D-8434-49B941DC1431}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleEnums", "tests\Generator\SampleEnums\SampleEnums.csproj", "{E948745F-D6D2-4455-9066-D128E846D711}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,6 +92,10 @@ Global {7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E}.Debug|Any CPU.Build.0 = Debug|Any CPU {7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E}.Release|Any CPU.ActiveCfg = Release|Any CPU {7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E}.Release|Any CPU.Build.0 = Release|Any CPU + {E948745F-D6D2-4455-9066-D128E846D711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E948745F-D6D2-4455-9066-D128E846D711}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E948745F-D6D2-4455-9066-D128E846D711}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E948745F-D6D2-4455-9066-D128E846D711}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2B03E852-A29E-4881-A013-BDC01C5021FF} = {EEBC4D9F-FCE9-4F37-8C48-7D9512302129} @@ -103,5 +111,7 @@ Global {2115BF90-CF61-40FF-840C-C7ED37CFF373} = {26FD7445-A8B3-45A5-85C1-FAAAF2C00335} {8C00EDB7-C432-4348-A8D4-E435DF85C891} = {2115BF90-CF61-40FF-840C-C7ED37CFF373} {7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E} = {06B1141F-5E62-4F18-9ADC-3D8205E1BA8A} + {52329321-E2A3-468D-8434-49B941DC1431} = {05C15E8B-5714-4CB9-96BB-5A2BA63EDF91} + {E948745F-D6D2-4455-9066-D128E846D711} = {52329321-E2A3-468D-8434-49B941DC1431} EndGlobalSection EndGlobal diff --git a/samples/EnumClass.RawEnumComparison/Program.cs b/samples/EnumClass.RawEnumComparison/Program.cs index 0203388..c8c4ebc 100644 --- a/samples/EnumClass.RawEnumComparison/Program.cs +++ b/samples/EnumClass.RawEnumComparison/Program.cs @@ -1,5 +1,8 @@ using System; +using EnumClass.Attributes; using EnumClass.SimpleEnum.EnumClass; + +[assembly: ExternalEnumClass(typeof(PetKind))] // ReSharper disable UnusedParameter.Local PrintEnumClassComparison("Dog", PetKind.Dog, EnumClass.SimpleEnum.PetKind.Dog); diff --git a/src/EnumClass.Attributes/ExternalEnumClassAttribute.cs b/src/EnumClass.Attributes/ExternalEnumClassAttribute.cs new file mode 100644 index 0000000..2fa6e62 --- /dev/null +++ b/src/EnumClass.Attributes/ExternalEnumClassAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace EnumClass.Attributes; + +/// +/// Marker attribute for EnumClassGenerator +/// used for generating EnumClass for enums in external assemblies +/// +[AttributeUsage(AttributeTargets.Assembly)] +public class ExternalEnumClassAttribute: Attribute +{ + /// + /// Primary constructor + /// + /// Type of enum for generating + public ExternalEnumClassAttribute(Type @enum) + { } + + /// + /// Namespace where generated class will be contained. + /// Defaults to namespace of original enum + "".EnumClass"" + /// + public string Namespace { get; set; } = null!; + + /// + /// Name of class that will be generated. + /// Defaults to the same name of enum + /// + public string ClassName { get; set; } = null!; +} \ No newline at end of file diff --git a/src/EnumClass.Core/EnumInfoFactory.cs b/src/EnumClass.Core/EnumInfoFactory.cs index ca08935..41d8510 100644 --- a/src/EnumClass.Core/EnumInfoFactory.cs +++ b/src/EnumClass.Core/EnumInfoFactory.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -34,10 +33,10 @@ public static EnumInfo CreateFromNamedTypeSymbol(INamedTypeSymbol enumSymbol, var underlyingType = GetUnderlyingType(enumSymbol); var accessibility = GetAccessibility(enumSymbol); - var resultNamespace = GetResultNamespace(enumSymbol, attributeInfo); + var resultNamespace = GetResultNamespace(enumSymbol, attributeInfo.Namespace); var @namespace = new ManuallySpecifiedSymbolName($"global::{resultNamespace}", resultNamespace); - var generatedClassName = GetClassName(enumSymbol, attributeInfo); + var generatedClassName = GetClassName(enumSymbol, attributeInfo.ClassName); var fullyQualifiedClassName = $"global::{resultNamespace}.{generatedClassName}"; var className = new ManuallySpecifiedSymbolName(fullyQualifiedClassName, generatedClassName); @@ -79,34 +78,34 @@ private static IAccessibility GetAccessibility(INamedTypeSymbol enumSymbol) /// Interface of underlying type private static IUnderlyingType GetUnderlyingType(INamedTypeSymbol enumSymbol) { - // This can not be null because enumSymbol is enum - // and for enum property EnumUnderlyingType must not be null - return enumSymbol.EnumUnderlyingType!.Name switch - { + // This can not be null because enumSymbol is enum + // and for enum property EnumUnderlyingType must not be null + return enumSymbol.EnumUnderlyingType!.Name switch + { - "Int32" => UnderlyingTypes.Int, - "Byte" => UnderlyingTypes.Byte, - "Int16" => UnderlyingTypes.Short, - "Int64" => UnderlyingTypes.Long, - "UInt64" => UnderlyingTypes.Ulong, - "SByte" => UnderlyingTypes.Sbyte, - "UInt16" => UnderlyingTypes.Ushort, - "UInt32" => UnderlyingTypes.Uint, - - // Fallback. - // Maybe better to throw exception or display diagnostic? - _ => UnderlyingTypes.Int - }; + "Int32" => UnderlyingTypes.Int, + "Byte" => UnderlyingTypes.Byte, + "Int16" => UnderlyingTypes.Short, + "Int64" => UnderlyingTypes.Long, + "UInt64" => UnderlyingTypes.Ulong, + "SByte" => UnderlyingTypes.Sbyte, + "UInt16" => UnderlyingTypes.Ushort, + "UInt32" => UnderlyingTypes.Uint, + + // Fallback. + // Maybe better to throw exception or display diagnostic? + _ => UnderlyingTypes.Int + }; } - private static string GetClassName(INamedTypeSymbol enumSymbol, EnumClassAttributeInfo info) + private static string GetClassName(INamedTypeSymbol enumSymbol, string? userDefinedClassName) { - return SymbolDisplay.FormatLiteral( info.ClassName ?? enumSymbol.Name, false ); + return SymbolDisplay.FormatLiteral( userDefinedClassName ?? enumSymbol.Name, false ); } - private static string GetResultNamespace(INamedTypeSymbol enumSymbol, EnumClassAttributeInfo attributeInfo) + private static string GetResultNamespace(INamedTypeSymbol enumSymbol, string? userDefinedNamespace) { - return attributeInfo.Namespace ?? enumSymbol.ContainingNamespace + return userDefinedNamespace ?? enumSymbol.ContainingNamespace .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) .Replace("global::", "") + ".EnumClass"; } @@ -228,4 +227,45 @@ bool IsMarkedWithEnumClassAttribute(INamedTypeSymbol enumTypeSymbol) return false; } } + + public static EnumInfo CreateFromExternalEnumNamedTypeSymbol(ExternalEnumClassAttributeInfo attributeInfo) + { + var enumSymbol = attributeInfo.EnumSymbol; + var members = attributeInfo.EnumSymbol.GetMembers(); + + var underlyingType = GetUnderlyingType(enumSymbol); + var accessibility = GetAccessibility(enumSymbol); + + var resultNamespace = GetResultNamespace(enumSymbol, attributeInfo.Namespace); + var @namespace = new ManuallySpecifiedSymbolName($"global::{resultNamespace}", resultNamespace); + + var generatedClassName = GetClassName(enumSymbol, attributeInfo.ClassName); + var fullyQualifiedClassName = $"global::{resultNamespace}.{generatedClassName}"; + var className = new ManuallySpecifiedSymbolName(fullyQualifiedClassName, generatedClassName); + + var fullyQualifiedEnumName = SymbolDisplay.ToDisplayString(enumSymbol, SymbolDisplayFormat.FullyQualifiedFormat); + var enumName = new ManuallySpecifiedSymbolName(fullyQualifiedEnumName, enumSymbol.Name); + + var memberInfos = members + .OfType() + .Combine(new EnumMemberInfoCreationContext(className, @namespace, enumName)) + // Skip all non enum fields declarations + // Enum members are all const, according to docs + .Where(static m => m.Left is {IsConst: true, HasConstantValue:true}) + // Try to convert them into EnumMemberInfo + .Select(p => EnumMemberInfoFactory.CreateFromFieldSymbol(p.Left, p.Right, null)!) + // And skip failed + .Where(static i => i is not null) + // Finally, create array of members + .ToArray(); + + + return new EnumInfo( + className, + enumName, + memberInfos, + underlyingType, + accessibility, + @namespace); + } } \ No newline at end of file diff --git a/src/EnumClass.Core/ExternalEnumClassAttributeInfo.cs b/src/EnumClass.Core/ExternalEnumClassAttributeInfo.cs new file mode 100644 index 0000000..c061994 --- /dev/null +++ b/src/EnumClass.Core/ExternalEnumClassAttributeInfo.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace EnumClass.Core; + +public record struct ExternalEnumClassAttributeInfo(INamedTypeSymbol EnumSymbol) +{ + public string? ClassName { get; set; } = null; + public string? Namespace { get; set; } = null; +} \ No newline at end of file diff --git a/src/EnumClass.Core/Infrastructure/Constants.cs b/src/EnumClass.Core/Infrastructure/Constants.cs index 0e4ab6c..b48a4d5 100644 --- a/src/EnumClass.Core/Infrastructure/Constants.cs +++ b/src/EnumClass.Core/Infrastructure/Constants.cs @@ -5,7 +5,8 @@ public static class Constants public static class EnumClassAttributeInfo { public const string AttributeFullName = "EnumClass.Attributes.EnumClassAttribute"; - + public const string AttributeClassName = "EnumClass"; + public static class NamedArguments { public const string Namespace = "Namespace"; @@ -22,4 +23,16 @@ public static class NamedArguments public const string StringValue = "StringValue"; } } + + // ReSharper disable once MemberHidesStaticFromOuterClass + public static class ExternalEnumClassAttributeInfo + { + public const string AttributeFullName = "EnumClass.Attributes.ExternalEnumClassAttribute"; + public const string AttributeClassName = "ExternalEnumClass"; + public static class NamedArguments + { + public const string Namespace = "Namespace"; + public const string ClassName = "ClassName"; + } + } } \ No newline at end of file diff --git a/src/EnumClass.Generator/EnumClass.Generator.csproj b/src/EnumClass.Generator/EnumClass.Generator.csproj index 4177944..0f9a27a 100644 --- a/src/EnumClass.Generator/EnumClass.Generator.csproj +++ b/src/EnumClass.Generator/EnumClass.Generator.csproj @@ -48,7 +48,7 @@ Features: True - + icon.png diff --git a/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs b/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs index bb1be79..ea8cc49 100644 --- a/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs +++ b/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs @@ -4,10 +4,10 @@ using EnumClass.Core; using EnumClass.Core.Infrastructure; using EnumClass.Core.Models; +using EnumClass.Generator.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; namespace EnumClass.Generator; @@ -16,21 +16,147 @@ public class EnumClassIncrementalGenerator: IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext generatorContext) { - IncrementalValuesProvider enums = - generatorContext - .SyntaxProvider - .CreateSyntaxProvider( - predicate: (node, _) => node is EnumDeclarationSyntax {AttributeLists.Count: > 0}, - transform: GetSemanticModelForEnumClass) - .Where(x => x is not null)!; + var directEnums = generatorContext + .SyntaxProvider + .CreateSyntaxProvider( + predicate: EnumDeclarationSyntaxPredicate, + transform: EnumDeclarationSyntaxTransform) + .Where(x => x is not null); - var provider = generatorContext.CompilationProvider.Combine(enums.Collect()); - generatorContext.RegisterSourceOutput(provider, (context, tuple) => GenerateAllEnumClasses(tuple.Left, tuple.Right, context)); + var provider = generatorContext.CompilationProvider.Combine(directEnums.Collect()); + generatorContext.RegisterSourceOutput(provider, (context, tuple) => GenerateEnumClasses(tuple.Left, tuple.Right!, context)); + + var externalEnums = generatorContext + .SyntaxProvider + .CreateSyntaxProvider( + predicate: ExternalEnumClassAttributePredicate, + transform: ExternalEnumClassAttributeTransform) + .Where(x => x is not null); + + generatorContext.RegisterSourceOutput(generatorContext.CompilationProvider.Combine(externalEnums.Collect()), + (context, tuple) => GenerateExternalEnumClass(tuple.Left, tuple.Right, context)); + } + + private static bool EnumDeclarationSyntaxPredicate(SyntaxNode node, CancellationToken _) + { + return node is EnumDeclarationSyntax + { + AttributeLists: {Count: > 0} attributes + } && + attributes.Any(attr => attr.Name.Contains(Constants.EnumClassAttributeInfo.AttributeClassName)); + } + + private static AttributeSyntax ExternalEnumClassAttributeTransform(GeneratorSyntaxContext context, CancellationToken _) + { + var attributes = ( ( AttributeListSyntax ) context.Node ).Attributes; + return attributes.FirstOrDefault(attr => + attr.Name + .ToFullString() + .Contains(Constants.ExternalEnumClassAttributeInfo.AttributeClassName))!; + } + + private static bool ExternalEnumClassAttributePredicate(SyntaxNode node, CancellationToken _) + { + return node is AttributeListSyntax {Attributes.Count: > 0} attributeListSyntax && + // [assembly: ... + ( attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.AssemblyKeyword) ?? false ) && + attributeListSyntax.Attributes.Any( + // [...ExternalEnumClass( + attribute => attribute.Name.Contains(Constants.ExternalEnumClassAttributeInfo.AttributeClassName) && + // typeof(T), ...] + attribute is + { + ArgumentList.Arguments: { Count: >0 } arguments + } && arguments[0].Expression.IsKind(SyntaxKind.TypeOfExpression)); } - private static void GenerateAllEnumClasses(Compilation compilation, - ImmutableArray enums, - SourceProductionContext context) + private void GenerateExternalEnumClass(Compilation compilation, + ImmutableArray attributes, + SourceProductionContext context) + { + if (attributes.IsDefaultOrEmpty) + { + return; + } + + var infos = new List(attributes.Length); + + // Collect all types that were marked to be generated + foreach (var attribute in attributes) + { + // Get enum type we want to construct + if (attribute is not + { + ArgumentList.Arguments: + { + Count: >0 + } arguments + } || + // First must be typeof(T) expression + arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpression) + { + continue; + } + + // Extract enum type + if (compilation.GetSemanticModel(attribute.SyntaxTree) + .GetSymbolInfo(typeOfExpression.Type) + .Symbol is not INamedTypeSymbol + { + // This is not null only for enums + EnumUnderlyingType: not null, + } enumType) + { + continue; + } + + var info = new ExternalEnumClassAttributeInfo(enumType); + + for (var i = 1; i < attribute.ArgumentList.Arguments.Count; i++) + { + var argument = attribute.ArgumentList.Arguments[i]; + if (argument.Expression is not LiteralExpressionSyntax + { + Token: + { + Value: not null, + ValueText: var value, + } token, + } + // Only accept single line string literals + || !token.IsKind(SyntaxKind.StringLiteralToken)) + { + continue; + } + + switch (argument.NameEquals?.Name.Identifier.Text) + { + case Constants.ExternalEnumClassAttributeInfo.NamedArguments.Namespace: + info = info with {Namespace = value}; + break; + case Constants.ExternalEnumClassAttributeInfo.NamedArguments.ClassName: + info = info with {ClassName = value}; + break; + } + } + + infos.Add(info); + } + + var generationContext = + new GenerationContext(compilation.Options.NullableContextOptions is not NullableContextOptions.Disable); + + foreach (var info in infos) + { + context.CancellationToken.ThrowIfCancellationRequested(); + var enumInfo = EnumInfoFactory.CreateFromExternalEnumNamedTypeSymbol(info); + GeneratorHelpers.GenerateEnumClass(enumInfo, context, generationContext); + } + } + + private static void GenerateEnumClasses(Compilation compilation, + ImmutableArray enums, + SourceProductionContext context) { // Do not use EnumInfoFactory that accepts compilation, // because it will search for all enums in all assemblies @@ -51,349 +177,18 @@ private static void GenerateAllEnumClasses(Compilation return; } - var nullableEnabled = compilation.Options.NullableContextOptions is not NullableContextOptions.Disable; - var builder = new StringBuilder(); + var generationContext = new GenerationContext( + nullableEnabled: compilation.Options.NullableContextOptions is not NullableContextOptions.Disable); + foreach (var enumInfo in enumInfos) { - builder.Clear(); - if (nullableEnabled) - { - // Source generated files should contain directive - builder.Append("#nullable enable\n\n"); - } - builder.AppendLine("using System;"); - builder.AppendLine("using System.Collections.Generic;"); - builder.AppendLine("using System.Runtime.CompilerServices;"); - builder.AppendLine(); - builder.AppendFormat("namespace {0}\n{{\n", enumInfo.Namespace); - builder.AppendLine(); - builder.AppendFormat("{2} abstract partial class {0}: " - + "IEquatable<{0}>, IEquatable<{1}>, " - + "IComparable<{0}>, IComparable<{1}>, IComparable\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName, enumInfo.Accessibility.Keyword); - builder.AppendLine("{"); - // Field of original enum we are wrapping - builder.AppendFormat(" protected readonly {0} _realEnumValue;\n", enumInfo.FullyQualifiedEnumName); - builder.AppendLine(); - - // Use for generating record init properties - // Constructor to initialize wrapped enum - builder.AppendFormat(" protected {0}({1} enumValue)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendLine(" this._realEnumValue = enumValue;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Cast to original enum - builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendFormat(" public static implicit operator {0}({1} value)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendLine(" return value._realEnumValue;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Cast to integer - builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendFormat(" public static explicit operator {0}({1} value)\n", enumInfo.UnderlyingType.CSharpKeyword, enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendFormat(" return ({0}) value._realEnumValue;\n", enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendLine(" }"); - builder.AppendLine(); - - // IEquatable for enum class - builder.AppendFormat(nullableEnabled - ? " public bool Equals({0}? other)\n" - : " public bool Equals({0} other)\n", enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendLine(" return !ReferenceEquals(other, null) && other._realEnumValue == this._realEnumValue;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // IEquatable for original enum - builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendFormat(" public bool Equals({0} other)\n", enumInfo.FullyQualifiedEnumName); - - builder.AppendLine(" {"); - builder.AppendLine(" return other == this._realEnumValue;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Generic equals override using IEquatable<> interfaces - builder.AppendLine(nullableEnabled - ? " public override bool Equals(object? other)" - : " public override bool Equals(object other)"); - builder.AppendLine(" {"); - // First check it is null or self - builder.AppendLine(" if (ReferenceEquals(other, null)) return false;"); - builder.AppendLine(" if (ReferenceEquals(other, this)) return true;"); - // Second check it is enum class instance - builder.AppendFormat(" if (other is {0})\n", enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendFormat(" return this.Equals(({0}) other);\n", enumInfo.ClassName); - builder.AppendLine(" }"); - // Then check it is raw original enum - builder.AppendFormat(" if (other is {0})\n", enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendFormat(" return this.Equals(({0}) other);\n", enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" }"); - // Otherwise return false - builder.AppendLine(" return false;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Create ==/!= operators for right raw original enum - builder.AppendFormat(" public static bool operator ==({0} left, {1} right)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendFormat(" return left.Equals(right);\n"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendFormat(" public static bool operator !=({0} left, {1} right)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendFormat(" return !left.Equals(right);\n"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Create ==/!= operators for left raw original enum - builder.AppendFormat(" public static bool operator ==({0} left, {1} right)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendFormat(" return right.Equals(left);\n"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendFormat(" public static bool operator !=({0} left, {1} right)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendFormat(" return !right.Equals(left);\n"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Generate GetHashCode - builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendLine(" public override int GetHashCode()"); - builder.AppendLine(" {"); - builder.AppendLine(" return this._realEnumValue.GetHashCode();"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Generate TryParse for string representation - { - var enumVariableName = enumInfo.GetVariableName(); - builder.AppendLine(nullableEnabled - ? $" public static bool TryParse(string value, out {enumInfo.ClassName}? {enumVariableName})" - : $" public static bool TryParse(string value, out {enumInfo.ClassName} {enumVariableName})"); - builder.AppendLine(" {"); - builder.AppendLine(" switch (value)"); - builder.AppendLine(" {"); - - // First, check for only enum member name. - // Then when enum name added. - // We do it in that way (not merging with enum name together), - // because usually we have only enum member name in string (my subjective opinion) - foreach (var member in enumInfo.Members) - { - builder.Append($" case \"{member.MemberName}\":\n"); - builder.Append($" {enumVariableName} = {member.MemberName};\n"); - builder.Append($" return true;\n"); - } - foreach (var member in enumInfo.Members) - { - builder.Append($" case \"{member.EnumMemberNameWithEnumName}\":\n"); - builder.Append($" {enumVariableName} = {member.MemberName};\n"); - builder.Append($" return true;\n"); - } - - builder.AppendLine(" }"); - builder.AppendLine($" {enumVariableName} = null;"); - builder.AppendLine(" return false;"); - builder.AppendLine(" }\n"); - } - builder.AppendLine(); - - // Generate TryParse for integral value - // Generate TryParse for string representation - { - var enumVariableName = enumInfo.GetVariableName(); - builder.AppendLine(nullableEnabled - ? $" public static bool TryParse({enumInfo.UnderlyingType.CSharpKeyword} value, out {enumInfo.ClassName}? {enumVariableName})" - : $" public static bool TryParse({enumInfo.UnderlyingType.CSharpKeyword} value, out {enumInfo.ClassName} {enumVariableName})"); - builder.AppendLine(" {"); - builder.AppendLine(" switch (value)"); - builder.AppendLine(" {"); - - // First, check for only enum member name. - // Then when enum name added. - // We do it in that way (not merging with enum name together), - // because usually we have only enum member name in string (my subjective opinion) - foreach (var member in enumInfo.Members) - { - builder.Append($" case {member.IntegralValue}:\n"); - builder.Append($" {enumVariableName} = {member.MemberName};\n"); - builder.Append($" return true;\n"); - } - - builder.AppendLine(" }"); - builder.AppendLine($" {enumVariableName} = null;"); - builder.AppendLine(" return false;"); - builder.AppendLine(" }\n"); - } - builder.AppendLine(); - - - // Implementations for IComparable interfaces - - // Enums implement IComparable.Compare(object) so there is allocation of value type (enum). - // Comparison by integral values has same semantics while remaining a faster option. - // Do not use subtraction as it may lead to overflow - - // IComparable - builder.AppendLine(nullableEnabled - ? " public int CompareTo(object? other)" - : " public int CompareTo(object other)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (ReferenceEquals(this, other)) return 0;"); - builder.AppendLine(" if (ReferenceEquals(null, other)) return 1;"); - builder.AppendFormat(" if (other is {0})\n", enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendFormat(" {0} temp = ({0}) other;\n", enumInfo.ClassName); - builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", - enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendFormat(" {0} right = (({0})temp._realEnumValue);\n", - enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); - builder.AppendLine(" }"); - // Cast passed object directly to int bypassing casting to original enum - builder.AppendFormat(" if (other is {0})\n", enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendFormat(" {0} right = (({0})other);\n", enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); - builder.AppendLine(" }"); - builder.AppendLine($" throw new ArgumentException($\"Object to compare must be either {{typeof({enumInfo.ClassName})}} or {{typeof({enumInfo.FullyQualifiedEnumName})}}. Given type: {{other.GetType()}}\", \"other\");"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // IComparable - builder.AppendFormat(nullableEnabled - ? " public int CompareTo({0}? other)\n" - : " public int CompareTo({0} other)\n", enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendLine(" if (ReferenceEquals(this, other)) return 0;"); - builder.AppendLine(" if (ReferenceEquals(null, other)) return 1;"); - builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", - enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendFormat(" {0} right = (({0})other._realEnumValue);\n", - enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // IComparable - builder.AppendFormat(" public int CompareTo({0} other)\n", enumInfo.FullyQualifiedEnumName); - builder.AppendLine(" {"); - builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendFormat(" {0} right = (({0})other);\n", enumInfo.UnderlyingType.CSharpKeyword); - builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); - builder.AppendLine(" }"); - builder.AppendLine(); - - - // Generate Switch definitions - var maxArgsCount = 8; - for (var i = 0; i < maxArgsCount; i++) - { - builder.AppendFormat(" public abstract {0};\n", enumInfo.GenerateSwitchActionDefinition(i)); - builder.AppendFormat(" public abstract {0};\n", enumInfo.GenerateSwitchFuncDefinition(i)); - } - - // Generate subclasses for each member of enum - foreach (var member in enumInfo.Members) - { - builder.AppendLine(); - // Generate static field for required Enum - builder.AppendFormat(" public static readonly {0} {1} = new {0}();\n", member.ClassName, member.MemberName); - - // Generate enum class for enum - builder.AppendFormat(" public partial class {0}: {1}\n", member.ClassName, enumInfo.ClassName); - builder.AppendLine(" {"); - - // Generate constructor - builder.AppendFormat(" public {0}(): base({1}) {{ }}\n", member.ClassName, member.FullyQualifiedEnumMemberName); - - // Override default ToString() - builder.AppendLine(" public override string ToString()"); - builder.AppendLine(" {"); - builder.AppendFormat(" return {0};\n", member.GetStringRepresentationQuoted()); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Generate Switches - for (var i = 0; i < maxArgsCount; i++) - { - // Action<> - builder.AppendFormat(" public override {0}\n", enumInfo.GenerateSwitchActionDefinition(i)); - builder.AppendLine(" {"); - - builder.AppendFormat(" {0}(this", member.GetSwitchArgName()); - for (var j = 0; j < i; j++) - { - builder.AppendFormat(", arg{0}", j); - } - - builder.AppendLine(");"); - builder.AppendLine(" }"); - builder.AppendLine(); - - // Func<> - builder.AppendFormat(" public override {0}\n", enumInfo.GenerateSwitchFuncDefinition(i)); - builder.AppendLine(" {"); - builder.AppendFormat(" return {0}(this", member.GetSwitchArgName()); - for (var j = 0; j < i; j++) - { - builder.AppendFormat(", arg{0}", j); - } - - builder.AppendLine(");"); - builder.AppendLine(" }"); - builder.AppendLine(); - } - - - builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendLine(" public override int GetHashCode()"); - builder.AppendLine(" {"); - builder.AppendFormat(" return {0};\n", enumInfo.UnderlyingType.ComputeHashCode(member.IntegralValue)); - builder.AppendLine(" }"); - - builder.AppendLine(" }"); - } - - builder.AppendLine(); - - // Generate method for iterating over all instances - builder.AppendFormat(" private static readonly {0}[] _members = new {0}[{1}] {{ ", enumInfo.ClassName, enumInfo.Members.Length); - - foreach (var member in enumInfo.Members) - { - builder.AppendFormat("{0}, ", member.MemberName); - } - - builder.AppendLine("};\n"); - - builder.AppendFormat(" public static System.Collections.Generic.IReadOnlyCollection<{0}> GetAllMembers()\n", enumInfo.ClassName); - builder.AppendLine(" {"); - builder.AppendLine(" return _members;"); - builder.AppendLine(" }"); - - // Enum class - builder.AppendLine("}"); - - // Namespace - builder.AppendLine("}"); - - // Create source file - context.AddSource($"{enumInfo.ClassName}.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); + context.CancellationToken.ThrowIfCancellationRequested(); + GeneratorHelpers.GenerateEnumClass(enumInfo, context, generationContext); } } [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach")] - private static EnumDeclarationSyntax? GetSemanticModelForEnumClass(GeneratorSyntaxContext context, CancellationToken token) + private static EnumDeclarationSyntax? EnumDeclarationSyntaxTransform(GeneratorSyntaxContext context, CancellationToken token) { var syntax = ( EnumDeclarationSyntax ) context.Node; diff --git a/src/EnumClass.Generator/GenerationContext.cs b/src/EnumClass.Generator/GenerationContext.cs new file mode 100644 index 0000000..d92f692 --- /dev/null +++ b/src/EnumClass.Generator/GenerationContext.cs @@ -0,0 +1,21 @@ +using System.Text; + +namespace EnumClass.Generator; + +public class GenerationContext +{ + private readonly StringBuilder _builder = new(); + + public GenerationContext(bool nullableEnabled) + { + NullableEnabled = nullableEnabled; + } + + public bool NullableEnabled { get; } + + public StringBuilder GetBuilder() + { + _builder.Clear(); + return _builder; + } +} \ No newline at end of file diff --git a/src/EnumClass.Generator/GeneratorHelpers.cs b/src/EnumClass.Generator/GeneratorHelpers.cs new file mode 100644 index 0000000..90cda44 --- /dev/null +++ b/src/EnumClass.Generator/GeneratorHelpers.cs @@ -0,0 +1,352 @@ +using System.Text; +using EnumClass.Core.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace EnumClass.Generator; + +public static class GeneratorHelpers +{ + public static void GenerateEnumClass(EnumInfo enumInfo, + SourceProductionContext productionContext, + GenerationContext context) + { + var nullableEnabled = context.NullableEnabled; + var builder = context.GetBuilder(); + + if (nullableEnabled) + { + // Source generated files should contain directive + builder.Append("#nullable enable\n\n"); + } + builder.AppendLine("using System;"); + builder.AppendLine("using System.Collections.Generic;"); + builder.AppendLine("using System.Runtime.CompilerServices;"); + builder.AppendLine(); + builder.AppendFormat("namespace {0}\n{{\n", enumInfo.Namespace); + builder.AppendLine(); + builder.AppendFormat("{2} abstract partial class {0}: " + + "IEquatable<{0}>, IEquatable<{1}>, " + + "IComparable<{0}>, IComparable<{1}>, IComparable\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName, enumInfo.Accessibility.Keyword); + builder.AppendLine("{"); + // Field of original enum we are wrapping + builder.AppendFormat(" protected readonly {0} _realEnumValue;\n", enumInfo.FullyQualifiedEnumName); + builder.AppendLine(); + + // Use for generating record init properties + // Constructor to initialize wrapped enum + builder.AppendFormat(" protected {0}({1} enumValue)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendLine(" this._realEnumValue = enumValue;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Cast to original enum + builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + builder.AppendFormat(" public static implicit operator {0}({1} value)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" return value._realEnumValue;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Cast to integer + builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + builder.AppendFormat(" public static explicit operator {0}({1} value)\n", enumInfo.UnderlyingType.CSharpKeyword, enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendFormat(" return ({0}) value._realEnumValue;\n", enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendLine(" }"); + builder.AppendLine(); + + // IEquatable for enum class + builder.AppendFormat(nullableEnabled + ? " public bool Equals({0}? other)\n" + : " public bool Equals({0} other)\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" return !ReferenceEquals(other, null) && other._realEnumValue == this._realEnumValue;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // IEquatable for original enum + builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + builder.AppendFormat(" public bool Equals({0} other)\n", enumInfo.FullyQualifiedEnumName); + + builder.AppendLine(" {"); + builder.AppendLine(" return other == this._realEnumValue;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Generic equals override using IEquatable<> interfaces + builder.AppendLine(nullableEnabled + ? " public override bool Equals(object? other)" + : " public override bool Equals(object other)"); + builder.AppendLine(" {"); + // First check it is null or self + builder.AppendLine(" if (ReferenceEquals(other, null)) return false;"); + builder.AppendLine(" if (ReferenceEquals(other, this)) return true;"); + // Second check it is enum class instance + builder.AppendFormat(" if (other is {0})\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendFormat(" return this.Equals(({0}) other);\n", enumInfo.ClassName); + builder.AppendLine(" }"); + // Then check it is raw original enum + builder.AppendFormat(" if (other is {0})\n", enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendFormat(" return this.Equals(({0}) other);\n", enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" }"); + // Otherwise return false + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Create ==/!= operators for right raw original enum + builder.AppendFormat(" public static bool operator ==({0} left, {1} right)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendFormat(" return left.Equals(right);\n"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendFormat(" public static bool operator !=({0} left, {1} right)\n", enumInfo.ClassName, enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendFormat(" return !left.Equals(right);\n"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Create ==/!= operators for left raw original enum + builder.AppendFormat(" public static bool operator ==({0} left, {1} right)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendFormat(" return right.Equals(left);\n"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendFormat(" public static bool operator !=({0} left, {1} right)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendFormat(" return !right.Equals(left);\n"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Generate GetHashCode + builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + builder.AppendLine(" public override int GetHashCode()"); + builder.AppendLine(" {"); + builder.AppendLine(" return this._realEnumValue.GetHashCode();"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Generate TryParse for string representation + { + var enumVariableName = enumInfo.GetVariableName(); + builder.AppendLine(nullableEnabled + ? $" public static bool TryParse(string value, out {enumInfo.ClassName}? {enumVariableName})" + : $" public static bool TryParse(string value, out {enumInfo.ClassName} {enumVariableName})"); + builder.AppendLine(" {"); + builder.AppendLine(" switch (value)"); + builder.AppendLine(" {"); + + // First, check for only enum member name. + // Then when enum name added. + // We do it in that way (not merging with enum name together), + // because usually we have only enum member name in string (my subjective opinion) + foreach (var member in enumInfo.Members) + { + builder.Append($" case \"{member.MemberName}\":\n"); + builder.Append($" {enumVariableName} = {member.MemberName};\n"); + builder.Append($" return true;\n"); + } + foreach (var member in enumInfo.Members) + { + builder.Append($" case \"{member.EnumMemberNameWithEnumName}\":\n"); + builder.Append($" {enumVariableName} = {member.MemberName};\n"); + builder.Append($" return true;\n"); + } + + builder.AppendLine(" }"); + builder.AppendLine($" {enumVariableName} = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }\n"); + } + builder.AppendLine(); + + // Generate TryParse for integral value + // Generate TryParse for string representation + { + var enumVariableName = enumInfo.GetVariableName(); + builder.AppendLine(nullableEnabled + ? $" public static bool TryParse({enumInfo.UnderlyingType.CSharpKeyword} value, out {enumInfo.ClassName}? {enumVariableName})" + : $" public static bool TryParse({enumInfo.UnderlyingType.CSharpKeyword} value, out {enumInfo.ClassName} {enumVariableName})"); + builder.AppendLine(" {"); + builder.AppendLine(" switch (value)"); + builder.AppendLine(" {"); + + // First, check for only enum member name. + // Then when enum name added. + // We do it in that way (not merging with enum name together), + // because usually we have only enum member name in string (my subjective opinion) + foreach (var member in enumInfo.Members) + { + builder.Append($" case {member.IntegralValue}:\n"); + builder.Append($" {enumVariableName} = {member.MemberName};\n"); + builder.Append($" return true;\n"); + } + + builder.AppendLine(" }"); + builder.AppendLine($" {enumVariableName} = null;"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }\n"); + } + builder.AppendLine(); + + + // Implementations for IComparable interfaces + + // Enums implement IComparable.Compare(object) so there is allocation of value type (enum). + // Comparison by integral values has same semantics while remaining a faster option. + // Do not use subtraction as it may lead to overflow + + // IComparable + builder.AppendLine(nullableEnabled + ? " public int CompareTo(object? other)" + : " public int CompareTo(object other)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (ReferenceEquals(this, other)) return 0;"); + builder.AppendLine(" if (ReferenceEquals(null, other)) return 1;"); + builder.AppendFormat(" if (other is {0})\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendFormat(" {0} temp = ({0}) other;\n", enumInfo.ClassName); + builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", + enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendFormat(" {0} right = (({0})temp._realEnumValue);\n", + enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); + builder.AppendLine(" }"); + // Cast passed object directly to int bypassing casting to original enum + builder.AppendFormat(" if (other is {0})\n", enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendFormat(" {0} right = (({0})other);\n", enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); + builder.AppendLine(" }"); + builder.AppendLine($" throw new ArgumentException($\"Object to compare must be either {{typeof({enumInfo.ClassName})}} or {{typeof({enumInfo.FullyQualifiedEnumName})}}. Given type: {{other.GetType()}}\", \"other\");"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // IComparable + builder.AppendFormat(nullableEnabled + ? " public int CompareTo({0}? other)\n" + : " public int CompareTo({0} other)\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" if (ReferenceEquals(this, other)) return 0;"); + builder.AppendLine(" if (ReferenceEquals(null, other)) return 1;"); + builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", + enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendFormat(" {0} right = (({0})other._realEnumValue);\n", + enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // IComparable + builder.AppendFormat(" public int CompareTo({0} other)\n", enumInfo.FullyQualifiedEnumName); + builder.AppendLine(" {"); + builder.AppendFormat(" {0} left = (({0})this._realEnumValue);\n", enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendFormat(" {0} right = (({0})other);\n", enumInfo.UnderlyingType.CSharpKeyword); + builder.AppendLine(" return left < right ? -1 : left == right ? 0 : 1;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + + // Generate Switch definitions + var maxArgsCount = 8; + for (var i = 0; i < maxArgsCount; i++) + { + builder.AppendFormat(" public abstract {0};\n", enumInfo.GenerateSwitchActionDefinition(i)); + builder.AppendFormat(" public abstract {0};\n", enumInfo.GenerateSwitchFuncDefinition(i)); + } + + // Generate subclasses for each member of enum + foreach (var member in enumInfo.Members) + { + builder.AppendLine(); + // Generate static field for required Enum + builder.AppendFormat(" public static readonly {0} {1} = new {0}();\n", member.ClassName, member.MemberName); + + // Generate enum class for enum + builder.AppendFormat(" public partial class {0}: {1}\n", member.ClassName, enumInfo.ClassName); + builder.AppendLine(" {"); + + // Generate constructor + builder.AppendFormat(" public {0}(): base({1}) {{ }}\n", member.ClassName, member.FullyQualifiedEnumMemberName); + + // Override default ToString() + builder.AppendLine(" public override string ToString()"); + builder.AppendLine(" {"); + builder.AppendFormat(" return {0};\n", member.GetStringRepresentationQuoted()); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Generate Switches + for (var i = 0; i < maxArgsCount; i++) + { + // Action<> + builder.AppendFormat(" public override {0}\n", enumInfo.GenerateSwitchActionDefinition(i)); + builder.AppendLine(" {"); + + builder.AppendFormat(" {0}(this", member.GetSwitchArgName()); + for (var j = 0; j < i; j++) + { + builder.AppendFormat(", arg{0}", j); + } + + builder.AppendLine(");"); + builder.AppendLine(" }"); + builder.AppendLine(); + + // Func<> + builder.AppendFormat(" public override {0}\n", enumInfo.GenerateSwitchFuncDefinition(i)); + builder.AppendLine(" {"); + builder.AppendFormat(" return {0}(this", member.GetSwitchArgName()); + for (var j = 0; j < i; j++) + { + builder.AppendFormat(", arg{0}", j); + } + + builder.AppendLine(");"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + + builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); + builder.AppendLine(" public override int GetHashCode()"); + builder.AppendLine(" {"); + builder.AppendFormat(" return {0};\n", enumInfo.UnderlyingType.ComputeHashCode(member.IntegralValue)); + builder.AppendLine(" }"); + + builder.AppendLine(" }"); + } + + builder.AppendLine(); + + // Generate method for iterating over all instances + builder.AppendFormat(" private static readonly {0}[] _members = new {0}[{1}] {{ ", enumInfo.ClassName, enumInfo.Members.Length); + + foreach (var member in enumInfo.Members) + { + builder.AppendFormat("{0}, ", member.MemberName); + } + + builder.AppendLine("};\n"); + + builder.AppendFormat(" public static System.Collections.Generic.IReadOnlyCollection<{0}> GetAllMembers()\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" return _members;"); + builder.AppendLine(" }"); + + // Enum class + builder.AppendLine("}"); + + // Namespace + builder.AppendLine("}"); + + // Create source file + productionContext.AddSource($"{enumInfo.ClassName}.g.cs", SourceText.From(builder.ToString(), Encoding.UTF8)); + } + +} \ No newline at end of file diff --git a/src/EnumClass.Generator/Infrastructure/NameSyntaxExtensions.cs b/src/EnumClass.Generator/Infrastructure/NameSyntaxExtensions.cs new file mode 100644 index 0000000..d462cc8 --- /dev/null +++ b/src/EnumClass.Generator/Infrastructure/NameSyntaxExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EnumClass.Generator.Infrastructure; + +public static class NameSyntaxExtensions +{ + public static bool Contains(this NameSyntax nameSyntax, string value) + { + return ( nameSyntax switch + { + IdentifierNameSyntax i => i.Identifier.Text, + SimpleNameSyntax s => s.Identifier.Text, + QualifiedNameSyntax q => q.Right.Identifier.Text, + _ => nameSyntax.ToFullString() + } ).Contains(value); + } +} \ No newline at end of file diff --git a/src/EnumClass.Generator/Infrastructure/SeparatedSyntaxListExtensions.cs b/src/EnumClass.Generator/Infrastructure/SeparatedSyntaxListExtensions.cs new file mode 100644 index 0000000..09a0707 --- /dev/null +++ b/src/EnumClass.Generator/Infrastructure/SeparatedSyntaxListExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; + +namespace EnumClass.Generator.Infrastructure; + +public static class SeparatedSyntaxListExtensions +{ + public static bool Any(this SeparatedSyntaxList list, Func filter) where T : SyntaxNode + { + for (var i = 0; i < list.Count; i++) + { + if (filter(list[i])) + { + return true; + } + } + + return false; + } + + public static T? FirstOrDefault(this SeparatedSyntaxList list, Func filter) where T : SyntaxNode + { + for (var i = 0; i < list.Count; i++) + { + if (filter(list[i])) + { + return list[i]; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/EnumClass.Generator/Infrastructure/SyntaxListExtensions.cs b/src/EnumClass.Generator/Infrastructure/SyntaxListExtensions.cs new file mode 100644 index 0000000..875ddc7 --- /dev/null +++ b/src/EnumClass.Generator/Infrastructure/SyntaxListExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EnumClass.Generator.Infrastructure; + +public static class SyntaxListExtensions +{ + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + public static bool Any(this SyntaxList list, Func predicate) + { + for (var i = 0; i < list.Count; i++) + { + var attributes = list[i].Attributes; + // ReSharper disable once ForCanBeConvertedToForeach + for (var j = 0; j < attributes.Count; j++) + { + if (predicate(attributes[j])) + { + return true; + } + } + } + + return false; + } +} \ No newline at end of file diff --git a/tests/Generator/EnumClass.Generator.Tests.Integration/EnumClass.Generator.Tests.Integration.csproj b/tests/Generator/EnumClass.Generator.Tests.Integration/EnumClass.Generator.Tests.Integration.csproj index 10c01f6..af98856 100644 --- a/tests/Generator/EnumClass.Generator.Tests.Integration/EnumClass.Generator.Tests.Integration.csproj +++ b/tests/Generator/EnumClass.Generator.Tests.Integration/EnumClass.Generator.Tests.Integration.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/Generator/EnumClass.Generator.Tests.Integration/ExternalEnumClassGeneratorTests.cs b/tests/Generator/EnumClass.Generator.Tests.Integration/ExternalEnumClassGeneratorTests.cs new file mode 100644 index 0000000..0504949 --- /dev/null +++ b/tests/Generator/EnumClass.Generator.Tests.Integration/ExternalEnumClassGeneratorTests.cs @@ -0,0 +1,31 @@ +using EnumClass.Attributes; +using SampleEnums; + +[assembly: ExternalEnumClass(typeof(Token))] +namespace EnumClass.Generator.Tests.Integration; + +public class ExternalEnumClassGeneratorTests +{ + public static SampleEnums.EnumClass.Token Keyword => SampleEnums.EnumClass.Token.Keyword; + public static SampleEnums.EnumClass.Token Identifier => SampleEnums.EnumClass.Token.Identifier; + public static SampleEnums.EnumClass.Token Word => SampleEnums.EnumClass.Token.Word; + public static SampleEnums.EnumClass.Token Trivia => SampleEnums.EnumClass.Token.Trivia; + + [Fact] + public void ToString_WithGeneratedClass_ShouldReturnMemberNames() + { + Assert.Equal("Keyword", Keyword.ToString()); + Assert.Equal("Identifier", Identifier.ToString()); + Assert.Equal("Word", Word.ToString()); + Assert.Equal("Trivia", Trivia.ToString()); + } + + [Fact] + public void EqualMembers__ShouldReturnTrueOnComparisonWithOriginalEnum() + { + Assert.True(Keyword == Token.Keyword); + Assert.True(Word == Token.Word); + Assert.True(Trivia == Token.Trivia); + Assert.True(Identifier == Token.Identifier); + } +} \ No newline at end of file diff --git a/tests/Generator/EnumClass.Generator.Tests.Integration/SwitchTests.cs b/tests/Generator/EnumClass.Generator.Tests.Integration/SwitchTests.cs index a342db3..7a2ba86 100644 --- a/tests/Generator/EnumClass.Generator.Tests.Integration/SwitchTests.cs +++ b/tests/Generator/EnumClass.Generator.Tests.Integration/SwitchTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using EnumClass.Attributes; @@ -12,6 +13,8 @@ public enum RoomState Cleaning } +// ReSharper disable once UnusedParameter.Local +[SuppressMessage("ReSharper", "UnusedParameter.Local")] public class SwitchTests { [Fact] @@ -27,4 +30,29 @@ public void ActionSwitch__WithFreeState__ShouldCallFreeHandler() Assert.True(called); } + + [Fact] + public void ActionSwitch__WithPassedValue__ShouldPassExactValueToHandlers() + { + var free = EnumClass.RoomState.Free; + var expected = 42; + free.Switch(expected, + (freeState, i) => Assert.Equal(expected, i), + (_, i) => Assert.True(false, "Should not be called"), + (_, i) => Assert.True(false, "Should not be called"), + (_, i) => Assert.True(false, "Should not be called")); + } + + [Fact] + public void FuncSwitch__WithCalculatedValue__ShouldReturnSpecifiedValue() + { + var state = EnumClass.RoomState.Free; + var expected = 42; + var actual = state.Switch( + free => expected, + _ => expected + 1, + _ => expected + 2, + _ => expected + 3); + Assert.Equal(expected, actual); + } } \ No newline at end of file diff --git a/tests/Generator/EnumClass.Generator.Tests/EnumClass.Generator.Tests.csproj b/tests/Generator/EnumClass.Generator.Tests/EnumClass.Generator.Tests.csproj index e086629..8569d96 100644 --- a/tests/Generator/EnumClass.Generator.Tests/EnumClass.Generator.Tests.csproj +++ b/tests/Generator/EnumClass.Generator.Tests/EnumClass.Generator.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/Generator/EnumClass.Generator.Tests/ExternalEnumClassAttributeTests.cs b/tests/Generator/EnumClass.Generator.Tests/ExternalEnumClassAttributeTests.cs new file mode 100644 index 0000000..a2ff23b --- /dev/null +++ b/tests/Generator/EnumClass.Generator.Tests/ExternalEnumClassAttributeTests.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using EnumClass.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using SampleEnums; + +namespace EnumClass.Generator.Tests; + +public class ExternalEnumClassAttributeTests +{ + [Fact] + public void WithSingleMember__ShouldGenerateWithoutErrors() + { + // References HelperProjects/SampleEnums/Token.cs + var source = @"using EnumClass.Attributes; +using SampleEnums; + +[assembly: ExternalEnumClass(typeof(Token), Namespace = ""SampleNamespace"")] +namespace Test; +"; + var compilation = CSharpCompilation.Create("Test", new[] {CSharpSyntaxTree.ParseText(source),}, + new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(EnumClassAttribute).Assembly.Location), + + MetadataReference.CreateFromFile(Assembly.GetCallingAssembly().Location), + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("System.Runtime")).Location), + MetadataReference.CreateFromFile(Assembly.Load(new AssemblyName("netstandard")).Location), + MetadataReference.CreateFromFile(typeof(Token).Assembly.Location), + }, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + CSharpGeneratorDriver.Create(new EnumClassIncrementalGenerator()) + .RunGeneratorsAndUpdateCompilation(compilation, out _, out var diagnostics); + Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)); + } +} \ No newline at end of file diff --git a/tests/Generator/SampleEnums/SampleEnums.csproj b/tests/Generator/SampleEnums/SampleEnums.csproj new file mode 100644 index 0000000..eb2460e --- /dev/null +++ b/tests/Generator/SampleEnums/SampleEnums.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/tests/Generator/SampleEnums/Token.cs b/tests/Generator/SampleEnums/Token.cs new file mode 100644 index 0000000..bfefb30 --- /dev/null +++ b/tests/Generator/SampleEnums/Token.cs @@ -0,0 +1,9 @@ +namespace SampleEnums; + +public enum Token +{ + Word, + Trivia, + Identifier, + Keyword +} \ No newline at end of file From 9e8d0d8c3a0676e7d0041e0a3748493cd34bf12d Mon Sep 17 00:00:00 2001 From: "ash.blade" Date: Fri, 9 Jun 2023 20:11:14 +0300 Subject: [PATCH 2/5] Add equality tests --- .../EnumClass.RawEnumComparison/Program.cs | 1 + .../EnumClassIncrementalGenerator.cs | 1 - src/EnumClass.Generator/GeneratorHelpers.cs | 19 +- .../ArrayExtensions.cs | 22 +++ .../ComparisonTests.cs | 169 ++++++++++++++++++ 5 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 tests/Generator/EnumClass.Generator.Tests.Integration/ArrayExtensions.cs create mode 100644 tests/Generator/EnumClass.Generator.Tests.Integration/ComparisonTests.cs diff --git a/samples/EnumClass.RawEnumComparison/Program.cs b/samples/EnumClass.RawEnumComparison/Program.cs index c8c4ebc..3c86e86 100644 --- a/samples/EnumClass.RawEnumComparison/Program.cs +++ b/samples/EnumClass.RawEnumComparison/Program.cs @@ -23,6 +23,7 @@ void PrintEnumClassComparison(string representationName, PetKind enumClassKind, Console.WriteLine($"Equals: {enumClassKind.Equals(enumKind)}"); Console.WriteLine($"==: {enumClassKind == enumKind}"); Console.WriteLine($"!=: {enumClassKind == enumKind}"); + // Console.WriteLine($"{enumClassKind == enumClassKind}"); var result = enumClassKind.Switch(2, 2, static (dog, i, j) => i + j * 2, diff --git a/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs b/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs index ea8cc49..df94ad7 100644 --- a/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs +++ b/src/EnumClass.Generator/EnumClassIncrementalGenerator.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Text; using EnumClass.Core; using EnumClass.Core.Infrastructure; using EnumClass.Core.Models; diff --git a/src/EnumClass.Generator/GeneratorHelpers.cs b/src/EnumClass.Generator/GeneratorHelpers.cs index 90cda44..2e795b7 100644 --- a/src/EnumClass.Generator/GeneratorHelpers.cs +++ b/src/EnumClass.Generator/GeneratorHelpers.cs @@ -43,7 +43,7 @@ public static void GenerateEnumClass(EnumInfo enumInfo, // Cast to original enum builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendFormat(" public static implicit operator {0}({1} value)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); + builder.AppendFormat(" public static explicit operator {0}({1} value)\n", enumInfo.FullyQualifiedEnumName, enumInfo.ClassName); builder.AppendLine(" {"); builder.AppendLine(" return value._realEnumValue;"); builder.AppendLine(" }"); @@ -51,7 +51,7 @@ public static void GenerateEnumClass(EnumInfo enumInfo, // Cast to integer builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); - builder.AppendFormat(" public static explicit operator {0}({1} value)\n", enumInfo.UnderlyingType.CSharpKeyword, enumInfo.ClassName); + builder.AppendFormat(" public static implicit operator {0}({1} value)\n", enumInfo.UnderlyingType.CSharpKeyword, enumInfo.ClassName); builder.AppendLine(" {"); builder.AppendFormat(" return ({0}) value._realEnumValue;\n", enumInfo.UnderlyingType.CSharpKeyword); builder.AppendLine(" }"); @@ -121,7 +121,20 @@ public static void GenerateEnumClass(EnumInfo enumInfo, builder.AppendFormat(" return !right.Equals(left);\n"); builder.AppendLine(" }"); builder.AppendLine(); - + + // Create ==/!= operators for both enum classes + builder.AppendFormat(" public static bool operator ==({0} left, {0} right)\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" return !ReferenceEquals(left, null) && left.Equals(right);"); + builder.AppendLine(" }"); + builder.AppendLine(); + + builder.AppendFormat(" public static bool operator !=({0} left, {0} right)\n", enumInfo.ClassName); + builder.AppendLine(" {"); + builder.AppendLine(" return ReferenceEquals(left, null) || !left.Equals(right);"); + builder.AppendLine(" }"); + builder.AppendLine(); + // Generate GetHashCode builder.AppendLine(" [MethodImpl(MethodImplOptions.AggressiveInlining)]"); builder.AppendLine(" public override int GetHashCode()"); diff --git a/tests/Generator/EnumClass.Generator.Tests.Integration/ArrayExtensions.cs b/tests/Generator/EnumClass.Generator.Tests.Integration/ArrayExtensions.cs new file mode 100644 index 0000000..66146f8 --- /dev/null +++ b/tests/Generator/EnumClass.Generator.Tests.Integration/ArrayExtensions.cs @@ -0,0 +1,22 @@ +namespace EnumClass.Generator.Tests.Integration; + +public static class ArrayExtensions +{ + public static IEnumerable<(T Value, T[] Others)> GetDifferent(this T[] array) + { + for (var i = 0; i < array.Length; i++) + { + var element = array[i]; + var rest = new List(array.Length - 1); + for (var j = 0; j < array.Length; j++) + { + if (j != i) + { + rest.Add(array[j]); + } + } + + yield return ( element, rest.ToArray() ); + } + } +} \ No newline at end of file diff --git a/tests/Generator/EnumClass.Generator.Tests.Integration/ComparisonTests.cs b/tests/Generator/EnumClass.Generator.Tests.Integration/ComparisonTests.cs new file mode 100644 index 0000000..2f8a252 --- /dev/null +++ b/tests/Generator/EnumClass.Generator.Tests.Integration/ComparisonTests.cs @@ -0,0 +1,169 @@ +using EnumClass.Attributes; + +namespace EnumClass.Generator.Tests.Integration; + +[EnumClass] +public enum LogLevel +{ + Debug, + Info, + Warning, + Error, + Fatal +} + +public class ComparisonTests +{ + public static readonly EnumClass.LogLevel Debug = EnumClass.LogLevel.Debug; + public static readonly EnumClass.LogLevel Info = EnumClass.LogLevel.Info; + public static readonly EnumClass.LogLevel Warning = EnumClass.LogLevel.Warning; + public static readonly EnumClass.LogLevel Error = EnumClass.LogLevel.Error; + public static readonly EnumClass.LogLevel Fatal = EnumClass.LogLevel.Fatal; + public static IEnumerable SameEnumClassMembers => EnumClass.LogLevel + .GetAllMembers() + .Select(x => new object[]{x}); + [Theory] + [MemberData(nameof(SameEnumClassMembers))] + public void EqualityOperator__WithSameEnumClass__ShouldReturnTrue(EnumClass.LogLevel level) + { + // ReSharper disable once EqualExpressionComparison + Assert.True(level == level); + } + + [Theory] + [MemberData(nameof(SameEnumClassMembers))] + public void Equals__WithSameEnumClass__ShouldReturnTrue(EnumClass.LogLevel level) + { + // ReSharper disable once EqualExpressionComparison + Assert.True(level.Equals(level)); + } + + public static IEnumerable EnumClassWithCorresponding => new[] + { + new object[] { Debug, LogLevel.Debug }, + new object[] { Info, LogLevel.Info }, + new object[] { Warning, LogLevel.Warning }, + new object[] { Error, LogLevel.Error }, + new object[] { Fatal, LogLevel.Fatal }, + }; + + [Theory] + [MemberData(nameof(EnumClassWithCorresponding))] + public void EqualityOperator__WithCorrespondingEnum__ShouldReturnTrue( + EnumClass.LogLevel level, + LogLevel corresponding) + { + Assert.True(level == corresponding); + } + + [Theory] + [MemberData(nameof(SameEnumClassMembers))] + public void EqualityOperator__WithNull__ShouldReturnFalse(EnumClass.LogLevel level) + { + Assert.False(level == null!); + Assert.False(null! == level!); + } + + [Theory] + [MemberData(nameof(SameEnumClassMembers))] + public void NeEqualityOperator__WithNull__ShouldReturnTrue(EnumClass.LogLevel level) + { + Assert.True(level != null!); + Assert.True(null! != level!); + } + + [Theory] + [MemberData(nameof(SameEnumClassMembers))] + public void Equals__WithNull__ShouldReturnFalse(EnumClass.LogLevel level) + { + Assert.False(level.Equals(null)); + } + + public static IEnumerable EnumClassWithDifferent => EnumClass.LogLevel + .GetAllMembers() + .ToArray() + .GetDifferent() + .Select(x => new object[]{x.Value, x.Others} ); + + [Theory] + [MemberData(nameof(EnumClassWithDifferent))] + public void EqualityOperator__WithDifferentEnumMembers__ShouldReturnFalse( + EnumClass.LogLevel value, + EnumClass.LogLevel[] rest) + { + foreach (var l in rest) + { + Assert.False(l == value); + Assert.False(value == l); + } + } + + [Theory] + [MemberData(nameof(EnumClassWithDifferent))] + public void NeEqualityOperator__WithDifferentEnumMembers__ShouldReturnTrue( + EnumClass.LogLevel value, + EnumClass.LogLevel[] rest) + { + foreach (var l in rest) + { + Assert.True(l != value); + Assert.True(value != l); + } + } + + [Theory] + [MemberData(nameof(EnumClassWithDifferent))] + public void Equals__WithDifferentEnumMembers__ShouldReturnFalse( + EnumClass.LogLevel value, + EnumClass.LogLevel[] rest) + { + foreach (var l in rest) + { + Assert.False(value.Equals(l)); + } + } + + public static IEnumerable EnumClassWithDifferentEnums => EnumClass.LogLevel + .GetAllMembers() + .ToArray() + .GetDifferent() + .Select(x => new object[]{x.Value, x.Others.Select(v => (LogLevel)v)} ); + + [Theory] + [MemberData(nameof(EnumClassWithDifferentEnums))] + public void EqualityOperator__WithDifferentRawEnums__ShouldReturnFalse( + EnumClass.LogLevel value, + IEnumerable rest) + { + foreach (var l in rest) + { + Assert.False(l == value); + Assert.False(value == l); + } + } + + [Theory] + [MemberData(nameof(EnumClassWithDifferentEnums))] + public void NeEqualityOperator__WithDifferentRawEnums__ShouldReturnTrue( + EnumClass.LogLevel value, + IEnumerable rest) + { + foreach (var l in rest) + { + Assert.True(l != value); + Assert.True(value != l); + } + } + + [Theory] + [MemberData(nameof(EnumClassWithDifferentEnums))] + public void Equals__WithDifferentRawEnums__ShouldReturnFalse( + EnumClass.LogLevel value, + IEnumerable rest) + { + foreach (var l in rest) + { + Assert.False(value.Equals(l)); + } + } +} \ No newline at end of file From 3db3678b638e54b75569cd0a8d5730afdf4e345c Mon Sep 17 00:00:00 2001 From: "ash.blade" Date: Fri, 9 Jun 2023 20:23:51 +0300 Subject: [PATCH 3/5] Add analyzer rules to generator project --- docs/AnalyzerReleases.Shipped.md | 10 ++++++++++ docs/AnalyzerReleases.Unshipped.md | 6 ------ src/EnumClass.Core/EnumClass.Core.csproj | 5 ----- src/EnumClass.Core/EnumInfoFactory.cs | 11 +++++++++-- src/EnumClass.Core/Infrastructure/Diagnostics.cs | 8 ++++++++ src/EnumClass.Generator/EnumClass.Generator.csproj | 4 +++- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/docs/AnalyzerReleases.Shipped.md b/docs/AnalyzerReleases.Shipped.md index e69de29..04cf6d6 100644 --- a/docs/AnalyzerReleases.Shipped.md +++ b/docs/AnalyzerReleases.Shipped.md @@ -0,0 +1,10 @@ +## Release 1.2.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +ECG001 | Usage | Warning | Diagnostics +ECG002 | Usage | Warning | Diagnostics +ECG003 | Usage | Warning | Diagnostics + diff --git a/docs/AnalyzerReleases.Unshipped.md b/docs/AnalyzerReleases.Unshipped.md index 8a6a171..e69de29 100644 --- a/docs/AnalyzerReleases.Unshipped.md +++ b/docs/AnalyzerReleases.Unshipped.md @@ -1,6 +0,0 @@ -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- -ECG001 | Usage | Warning | Diagnostics -ECG002 | Usage | Warning | Diagnostics \ No newline at end of file diff --git a/src/EnumClass.Core/EnumClass.Core.csproj b/src/EnumClass.Core/EnumClass.Core.csproj index 3971195..3be00fc 100644 --- a/src/EnumClass.Core/EnumClass.Core.csproj +++ b/src/EnumClass.Core/EnumClass.Core.csproj @@ -10,9 +10,4 @@ - - - - - diff --git a/src/EnumClass.Core/EnumInfoFactory.cs b/src/EnumClass.Core/EnumInfoFactory.cs index 41d8510..91b51cb 100644 --- a/src/EnumClass.Core/EnumInfoFactory.cs +++ b/src/EnumClass.Core/EnumInfoFactory.cs @@ -188,20 +188,27 @@ private static EnumClassAttributeInfo ExtractEnumClassAttributeCtorInfo(INamedTy SourceProductionContext context) { var enumClassAttribute = compilation.GetTypeByMetadataName(Constants.EnumClassAttributeInfo.AttributeFullName); - var enumMemberInfoAttribute = compilation.GetTypeByMetadataName(Constants.EnumMemberInfoAttributeInfo.AttributeFullName); - if (enumClassAttribute is null) { context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoEnumClassAttributeFound, Location.None)); return null; } + var enumMemberInfoAttribute = compilation.GetTypeByMetadataName(Constants.EnumMemberInfoAttributeInfo.AttributeFullName); if (enumMemberInfoAttribute is null) { context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoEnumMemberInfoAttributeFound, Location.None)); return null; } + var externalEnumInfoAttribute = + compilation.GetTypeByMetadataName(Constants.ExternalEnumClassAttributeInfo.AttributeFullName); + + if (externalEnumInfoAttribute is null) + { + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.NoExternalEnumClassAttributeFound, Location.None)); + } + var parsed = new List(); foreach (var namedTypeSymbol in FactoryHelpers.ExtractAllEnumsFromCompilation(compilation)) diff --git a/src/EnumClass.Core/Infrastructure/Diagnostics.cs b/src/EnumClass.Core/Infrastructure/Diagnostics.cs index f942c76..fa59d55 100644 --- a/src/EnumClass.Core/Infrastructure/Diagnostics.cs +++ b/src/EnumClass.Core/Infrastructure/Diagnostics.cs @@ -19,4 +19,12 @@ public static class Diagnostics "Usage", DiagnosticSeverity.Warning, true); + + public static readonly DiagnosticDescriptor NoExternalEnumClassAttributeFound = + new("ECG003", + "No ExternalEnumClassAttribute found", + "Could not find ExternalEnumClassAttribute: add reference to package with EnumClass.Generator, update nuget package to 1.3.0 or add package directly", + "Usage", + DiagnosticSeverity.Warning, + true); } \ No newline at end of file diff --git a/src/EnumClass.Generator/EnumClass.Generator.csproj b/src/EnumClass.Generator/EnumClass.Generator.csproj index 0f9a27a..2bcb475 100644 --- a/src/EnumClass.Generator/EnumClass.Generator.csproj +++ b/src/EnumClass.Generator/EnumClass.Generator.csproj @@ -23,7 +23,7 @@ Features: https://github.com/ashenBlade/EnumClass Apache-2.0 README.md - 1.2.0 + 1.3.0 icon.png @@ -52,6 +52,8 @@ Features: icon.png + + From b74bc7297d3d99a72f382d0b40d5b9e10a86f9ac Mon Sep 17 00:00:00 2001 From: "ash.blade" Date: Fri, 9 Jun 2023 20:28:35 +0300 Subject: [PATCH 4/5] update AnalyzerReleases.Shipped.md --- docs/AnalyzerReleases.Shipped.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/AnalyzerReleases.Shipped.md b/docs/AnalyzerReleases.Shipped.md index 04cf6d6..70f61d9 100644 --- a/docs/AnalyzerReleases.Shipped.md +++ b/docs/AnalyzerReleases.Shipped.md @@ -1,3 +1,6 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + ## Release 1.2.0 ### New Rules @@ -6,5 +9,11 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- ECG001 | Usage | Warning | Diagnostics ECG002 | Usage | Warning | Diagnostics -ECG003 | Usage | Warning | Diagnostics +## Release 1.3.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +ECG003 | Usage | Warning | Diagnostics From 58b48c15c46830e3594cb3039b35f3a1a7460ce1 Mon Sep 17 00:00:00 2001 From: "ash.blade" Date: Fri, 9 Jun 2023 20:45:02 +0300 Subject: [PATCH 5/5] Add example for generating enum class from external assembly --- EnumClass.sln | 17 +++++++ README.md | 48 +++++++++++++++++++ .../EnumClass.EnumOnly.csproj | 9 ++++ samples/EnumClass.EnumOnly/Toy.cs | 8 ++++ .../EnumClass.FromAnotherAssembly.csproj | 16 +++++++ .../EnumClass.FromAnotherAssembly/Program.cs | 9 ++++ samples/EnumClass.FromAnotherAssembly/Toy.cs | 33 +++++++++++++ src/EnumClass.Core/EnumClass.Core.csproj | 2 + 8 files changed, 142 insertions(+) create mode 100644 samples/EnumClass.EnumOnly/EnumClass.EnumOnly.csproj create mode 100644 samples/EnumClass.EnumOnly/Toy.cs create mode 100644 samples/EnumClass.FromAnotherAssembly/EnumClass.FromAnotherAssembly.csproj create mode 100644 samples/EnumClass.FromAnotherAssembly/Program.cs create mode 100644 samples/EnumClass.FromAnotherAssembly/Toy.cs diff --git a/EnumClass.sln b/EnumClass.sln index 828897f..4b1323d 100644 --- a/EnumClass.sln +++ b/EnumClass.sln @@ -39,6 +39,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HelperProjects", "HelperPro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleEnums", "tests\Generator\SampleEnums\SampleEnums.csproj", "{E948745F-D6D2-4455-9066-D128E846D711}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FromExternalAssembly", "FromExternalAssembly", "{2E55B6B6-0217-45DB-B097-28589F21767C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumClass.FromAnotherAssembly", "samples\EnumClass.FromAnotherAssembly\EnumClass.FromAnotherAssembly.csproj", "{7ACE470B-C303-4F55-AFAF-12178860A48E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumClass.EnumOnly", "samples\EnumClass.EnumOnly\EnumClass.EnumOnly.csproj", "{7932ED00-F088-4227-8BAB-22FB30966395}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +102,14 @@ Global {E948745F-D6D2-4455-9066-D128E846D711}.Debug|Any CPU.Build.0 = Debug|Any CPU {E948745F-D6D2-4455-9066-D128E846D711}.Release|Any CPU.ActiveCfg = Release|Any CPU {E948745F-D6D2-4455-9066-D128E846D711}.Release|Any CPU.Build.0 = Release|Any CPU + {7ACE470B-C303-4F55-AFAF-12178860A48E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7ACE470B-C303-4F55-AFAF-12178860A48E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7ACE470B-C303-4F55-AFAF-12178860A48E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7ACE470B-C303-4F55-AFAF-12178860A48E}.Release|Any CPU.Build.0 = Release|Any CPU + {7932ED00-F088-4227-8BAB-22FB30966395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7932ED00-F088-4227-8BAB-22FB30966395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7932ED00-F088-4227-8BAB-22FB30966395}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7932ED00-F088-4227-8BAB-22FB30966395}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2B03E852-A29E-4881-A013-BDC01C5021FF} = {EEBC4D9F-FCE9-4F37-8C48-7D9512302129} @@ -113,5 +127,8 @@ Global {7BE2B7EC-E2E7-4FC5-9403-0CB536C8528E} = {06B1141F-5E62-4F18-9ADC-3D8205E1BA8A} {52329321-E2A3-468D-8434-49B941DC1431} = {05C15E8B-5714-4CB9-96BB-5A2BA63EDF91} {E948745F-D6D2-4455-9066-D128E846D711} = {52329321-E2A3-468D-8434-49B941DC1431} + {2E55B6B6-0217-45DB-B097-28589F21767C} = {06B1141F-5E62-4F18-9ADC-3D8205E1BA8A} + {7ACE470B-C303-4F55-AFAF-12178860A48E} = {2E55B6B6-0217-45DB-B097-28589F21767C} + {7932ED00-F088-4227-8BAB-22FB30966395} = {2E55B6B6-0217-45DB-B097-28589F21767C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 2d414dd..8ea01fc 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,54 @@ using Domain; Console.WriteLine(SampleEnum.First); ``` +### Generate `enum class` for enum from another assembly + +If you do not have access to enum source code directly, you can generate `enum class` for enum in external assembly. +For this use `[ExternalEnumClass]` attribute. + +```csharp +// External assembly +namespace Logic; + +public enum Word +{ + Single, + Double, + Triple +} +``` + +```csharp +// Our assembly +using EnumClass.Attributes; +using Logic; + +[assembly: ExternalEnumClass(typeof(Word), Namespace = "Another")] +namespace Another; + +public partial class Word +{ + public abstract int WordsCount { get; } + + public partial class SingleEnumValue + { + public override int WordsCount => 1; + } + + + public partial class DoubleEnumValue + { + public override int WordsCount => 2; + } + + + public partial class TripleEnumValue + { + public override int WordsCount => 3; + } +} +``` + ## Known limitations ### Same name of member and enum diff --git a/samples/EnumClass.EnumOnly/EnumClass.EnumOnly.csproj b/samples/EnumClass.EnumOnly/EnumClass.EnumOnly.csproj new file mode 100644 index 0000000..eb2460e --- /dev/null +++ b/samples/EnumClass.EnumOnly/EnumClass.EnumOnly.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/samples/EnumClass.EnumOnly/Toy.cs b/samples/EnumClass.EnumOnly/Toy.cs new file mode 100644 index 0000000..8abe645 --- /dev/null +++ b/samples/EnumClass.EnumOnly/Toy.cs @@ -0,0 +1,8 @@ +namespace EnumClass.EnumOnly; + +public enum Toy +{ + Car, + Doll, + Ball +} \ No newline at end of file diff --git a/samples/EnumClass.FromAnotherAssembly/EnumClass.FromAnotherAssembly.csproj b/samples/EnumClass.FromAnotherAssembly/EnumClass.FromAnotherAssembly.csproj new file mode 100644 index 0000000..8ac4052 --- /dev/null +++ b/samples/EnumClass.FromAnotherAssembly/EnumClass.FromAnotherAssembly.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + diff --git a/samples/EnumClass.FromAnotherAssembly/Program.cs b/samples/EnumClass.FromAnotherAssembly/Program.cs new file mode 100644 index 0000000..18beb26 --- /dev/null +++ b/samples/EnumClass.FromAnotherAssembly/Program.cs @@ -0,0 +1,9 @@ +using System.Threading.Channels; +using EnumClass.FromAnotherAssembly; + +foreach (var member in Toy.GetAllMembers()) +{ + Console.WriteLine($"Playing with {member}:"); + member.Play(); + Console.WriteLine(); +} \ No newline at end of file diff --git a/samples/EnumClass.FromAnotherAssembly/Toy.cs b/samples/EnumClass.FromAnotherAssembly/Toy.cs new file mode 100644 index 0000000..e08b617 --- /dev/null +++ b/samples/EnumClass.FromAnotherAssembly/Toy.cs @@ -0,0 +1,33 @@ +using EnumClass.Attributes; +using Toy = EnumClass.EnumOnly.Toy; + +[assembly: ExternalEnumClass(typeof(Toy), Namespace = "EnumClass.FromAnotherAssembly")] +namespace EnumClass.FromAnotherAssembly; + +public partial class Toy +{ + public abstract void Play(); + public partial class BallEnumValue + { + public override void Play() + { + Console.WriteLine($"Ball jumps high"); + } + } + + public partial class CarEnumValue + { + public override void Play() + { + Console.WriteLine($"Car is going fast"); + } + } + + public partial class DollEnumValue + { + public override void Play() + { + Console.WriteLine($"Doll is dressed smartly"); + } + } +} \ No newline at end of file diff --git a/src/EnumClass.Core/EnumClass.Core.csproj b/src/EnumClass.Core/EnumClass.Core.csproj index 3be00fc..14fd51c 100644 --- a/src/EnumClass.Core/EnumClass.Core.csproj +++ b/src/EnumClass.Core/EnumClass.Core.csproj @@ -9,5 +9,7 @@ + +