diff --git a/Lombok.NET.Test/EnumValuesTest.cs b/Lombok.NET.Test/EnumValuesTest.cs new file mode 100644 index 0000000..bc2529a --- /dev/null +++ b/Lombok.NET.Test/EnumValuesTest.cs @@ -0,0 +1,22 @@ +using Xunit; + +namespace Lombok.NET.Test; + +public sealed class EnumValuesTest +{ + [Fact] + public void Test() + { + var values = MyEnumValues.Values; + Assert.Equal(3, values.Length); + Assert.Equal(MyEnum.One, values[0]); + Assert.Equal(MyEnum.Two, values[1]); + Assert.Equal(MyEnum.Three, values[2]); + } +} + +[EnumValues] +public enum MyEnum +{ + One, Two, Three +} \ No newline at end of file diff --git a/Lombok.NET.Test/Lombok.NET.Test.csproj b/Lombok.NET.Test/Lombok.NET.Test.csproj index 368e2d1..50d0a0d 100644 --- a/Lombok.NET.Test/Lombok.NET.Test.csproj +++ b/Lombok.NET.Test/Lombok.NET.Test.csproj @@ -10,10 +10,13 @@ - - + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Lombok.NET/Extensions/SyntaxNodeExtensions.cs b/Lombok.NET/Extensions/SyntaxNodeExtensions.cs index 4061941..a3f4ffd 100644 --- a/Lombok.NET/Extensions/SyntaxNodeExtensions.cs +++ b/Lombok.NET/Extensions/SyntaxNodeExtensions.cs @@ -234,7 +234,7 @@ public static bool IsVoid(this TypeSyntax typeSyntax) /// /// The type to check. /// True, if the type is declared within another type. - public static bool IsNestedType(this TypeDeclarationSyntax typeDeclaration) + public static bool IsNestedType(this BaseTypeDeclarationSyntax typeDeclaration) { return typeDeclaration.Parent is TypeDeclarationSyntax; } @@ -311,7 +311,7 @@ public static IEnumerable Where(this IEnumerable members, AccessTypes a /// The type to get the name for /// The namespace which will be prepended to the type using underscores. /// A unique name for the type inside a generator context. - public static string GetHintName(this TypeDeclarationSyntax type, NameSyntax @namespace) + public static string GetHintName(this BaseTypeDeclarationSyntax type, NameSyntax @namespace) { return string.Concat(@namespace.ToString().Replace('.', '_'), '_', type.Identifier.Text); } diff --git a/Lombok.NET/PropertyGenerators/EnumValuesGenerator.cs b/Lombok.NET/PropertyGenerators/EnumValuesGenerator.cs new file mode 100644 index 0000000..e1e49ad --- /dev/null +++ b/Lombok.NET/PropertyGenerators/EnumValuesGenerator.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using Lombok.NET.Analyzers; +using Lombok.NET.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +#if DEBUG +using System.Diagnostics; +#endif + +namespace Lombok.NET.PropertyGenerators; + +/// +/// Generator which generates a property containing an enum's values. +/// +[Generator] +internal sealed class EnumValuesGenerator : IIncrementalGenerator +{ + private static readonly string AttributeName = typeof(EnumValuesAttribute).FullName; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if DEBUG + SpinWait.SpinUntil(static () => Debugger.IsAttached); +#endif + var sources = context.SyntaxProvider.ForAttributeWithMetadataName(AttributeName, IsCandidate, Transform); + context.AddSources(sources); + } + + private static bool IsCandidate(SyntaxNode node, CancellationToken cancellationToken) + { + return node is EnumDeclarationSyntax; + } + + private static GeneratorResult Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var enumDeclaration = (EnumDeclarationSyntax)context.TargetNode; + var @namespace = enumDeclaration.GetNamespace(); + if (@namespace is null) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeMustHaveNamespace, enumDeclaration.GetLocation(), enumDeclaration.Identifier.Text); + + return new GeneratorResult(diagnostic); + } + + if (enumDeclaration.IsNestedType()) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.TypeMustBeNonNested, enumDeclaration.GetLocation(), enumDeclaration.Identifier.Text); + + return new GeneratorResult(diagnostic); + } + + var enumIdentifier = enumDeclaration.Identifier; + var valuesClassName = context.Attributes[0].NamedArguments.FirstOrDefault(kv => kv.Key == nameof(EnumValuesAttribute.TypeName)).Value.Value as string + ?? enumIdentifier.Text + "Values"; + + var enumMembers = GetEnumMemberAccessExpressions(enumIdentifier, enumDeclaration.Members); + + cancellationToken.ThrowIfCancellationRequested(); + + var sourceText = @namespace.CreateNewNamespace( + default, + ClassDeclaration(valuesClassName) + .WithModifiers( + TokenList( + Token(enumDeclaration.GetAccessibilityModifier()), + Token(SyntaxKind.StaticKeyword) + ) + ).WithMembers( + SingletonList( + PropertyDeclaration( + ArrayType( + IdentifierName(enumIdentifier) + ) + .WithRankSpecifiers( + SingletonList( + ArrayRankSpecifier( + SingletonSeparatedList( + OmittedArraySizeExpression() + ) + ) + ) + ), + Identifier("Values") + ).WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword)) + ).WithAccessorList( + AccessorList( + SingletonList( + AccessorDeclaration( + SyntaxKind.GetAccessorDeclaration + ) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken) + ) + ) + ) + ).WithInitializer( + EqualsValueClause( + InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + SeparatedList(enumMembers) + ) + ) + ).WithSemicolonToken( + Token(SyntaxKind.SemicolonToken) + ) + ) + ) + ).NormalizeWhitespace() + .GetText(Encoding.UTF8); + var hintName = enumDeclaration.GetHintName(@namespace); + + return new GeneratorResult(hintName, sourceText); + } + + private static IEnumerable GetEnumMemberAccessExpressions(SyntaxToken enumIdentifier, SeparatedSyntaxList enumMembers) + { + foreach (var enumMember in enumMembers) + { + yield return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(enumIdentifier), + IdentifierName(enumMember.Identifier) + ); + yield return Token(SyntaxKind.CommaToken); + } + } +} \ No newline at end of file diff --git a/Lombok.NET/SourceGeneratorAttributes.cs b/Lombok.NET/SourceGeneratorAttributes.cs index 9639c65..4a62d60 100644 --- a/Lombok.NET/SourceGeneratorAttributes.cs +++ b/Lombok.NET/SourceGeneratorAttributes.cs @@ -175,6 +175,18 @@ public sealed class FreezableAttribute : Attribute public bool IsUnfreezable { get; set; } = true; } +/// +/// Tells Lombok.NET to generate a static class with a property which returns all of the enum's values. +/// +[AttributeUsage(AttributeTargets.Enum)] +public sealed class EnumValuesAttribute : Attribute +{ + /// + /// When specified, allows to override the name of the static class which is generated, in order to avoid name collisions. Defaults to the enum's name + "Values" + /// + public string TypeName { get; set; } = default!; +} + /// /// The kind of members which Lombok.NET supports. ///