Skip to content

Commit

Permalink
Add EnumValuesGenerator. Fixes #4
Browse files Browse the repository at this point in the history
  • Loading branch information
CollinAlpert committed Dec 28, 2023
1 parent dbc6018 commit 106e493
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 5 deletions.
22 changes: 22 additions & 0 deletions Lombok.NET.Test/EnumValuesTest.cs
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 6 additions & 3 deletions Lombok.NET.Test/Lombok.NET.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="ReactiveUI" Version="18.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Lombok.NET/Extensions/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public static bool IsVoid(this TypeSyntax typeSyntax)
/// </summary>
/// <param name="typeDeclaration">The type to check.</param>
/// <returns>True, if the type is declared within another type.</returns>
public static bool IsNestedType(this TypeDeclarationSyntax typeDeclaration)
public static bool IsNestedType(this BaseTypeDeclarationSyntax typeDeclaration)
{
return typeDeclaration.Parent is TypeDeclarationSyntax;
}
Expand Down Expand Up @@ -311,7 +311,7 @@ public static IEnumerable<T> Where<T>(this IEnumerable<T> members, AccessTypes a
/// <param name="type">The type to get the name for</param>
/// <param name="namespace">The namespace which will be prepended to the type using underscores.</param>
/// <returns>A unique name for the type inside a generator context.</returns>
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);
}
Expand Down
136 changes: 136 additions & 0 deletions Lombok.NET/PropertyGenerators/EnumValuesGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Generator which generates a property containing an enum's values.
/// </summary>
[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<MemberDeclarationSyntax>(
PropertyDeclaration(
ArrayType(
IdentifierName(enumIdentifier)
)
.WithRankSpecifiers(
SingletonList(
ArrayRankSpecifier(
SingletonSeparatedList<ExpressionSyntax>(
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<ExpressionSyntax>(enumMembers)
)
)
).WithSemicolonToken(
Token(SyntaxKind.SemicolonToken)
)
)
)
).NormalizeWhitespace()
.GetText(Encoding.UTF8);
var hintName = enumDeclaration.GetHintName(@namespace);

return new GeneratorResult(hintName, sourceText);
}

private static IEnumerable<SyntaxNodeOrToken> GetEnumMemberAccessExpressions(SyntaxToken enumIdentifier, SeparatedSyntaxList<EnumMemberDeclarationSyntax> enumMembers)
{
foreach (var enumMember in enumMembers)
{
yield return MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(enumIdentifier),
IdentifierName(enumMember.Identifier)
);
yield return Token(SyntaxKind.CommaToken);
}
}
}
12 changes: 12 additions & 0 deletions Lombok.NET/SourceGeneratorAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,18 @@ public sealed class FreezableAttribute : Attribute
public bool IsUnfreezable { get; set; } = true;
}

/// <summary>
/// Tells Lombok.NET to generate a static class with a property which returns all of the enum's values.
/// </summary>
[AttributeUsage(AttributeTargets.Enum)]
public sealed class EnumValuesAttribute : Attribute
{
/// <summary>
/// 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"
/// </summary>
public string TypeName { get; set; } = default!;
}

/// <summary>
/// The kind of members which Lombok.NET supports.
/// </summary>
Expand Down

0 comments on commit 106e493

Please sign in to comment.