Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: introduce source generator for enum code generation #585

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f09b927
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
8dee215
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
150a665
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
03caf87
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
cceb42f
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
9e288b3
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
0ecb009
feature: introduce source generator for enum code generation
Meir017 Aug 11, 2024
1da53f7
feature: introduce source generator for enum code generation
Meir017 Aug 12, 2024
69d506b
feature: introduce source generator for enum code generation
Meir017 Aug 12, 2024
245492a
feature: introduce source generator for enum code generation
Meir017 Aug 12, 2024
39e5b5d
feature: introduce source generator for enum code generation
Meir017 Aug 12, 2024
82e9faf
feature: introduce source generator for enum code generation
Meir017 Aug 12, 2024
6ae0c75
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 13, 2024
e81058c
migrate to IIncrementalGenerator
Meir017 Aug 13, 2024
c0a1af5
Merge branch 'feature/enum-source-generator' of https://github.com/Me…
Meir017 Aug 13, 2024
5bea62d
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 14, 2024
c64790d
feature: introduce source generator for enum code generation
Meir017 Aug 14, 2024
bdde751
feature: introduce source generator for enum code generation
Meir017 Aug 14, 2024
bf44e78
feature: introduce source generator for enum code generation
Meir017 Aug 14, 2024
3b70c2f
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 15, 2024
7c09212
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 15, 2024
52befc0
Merge branch 'main' into feature/enum-source-generator
TalZaccai Aug 15, 2024
123ecc8
change tfm to netstandard
Meir017 Aug 25, 2024
943283d
Merge remote-tracking branch 'origin/main' into feature/enum-source-g…
Meir017 Aug 25, 2024
c7dd327
merge
Meir017 Aug 25, 2024
b018d62
merge
Meir017 Aug 25, 2024
ff931da
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 26, 2024
abf98c9
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 28, 2024
26a758d
Merge branch 'main' into feature/enum-source-generator
Meir017 Aug 30, 2024
3a65347
Merge branch 'main' into feature/enum-source-generator
Meir017 Sep 12, 2024
4157234
Merge remote-tracking branch 'origin/main' into feature/enum-source-g…
Meir017 Oct 20, 2024
63b7e2e
Merge branch 'feature/enum-source-generator' of https://github.com/me…
Meir017 Oct 20, 2024
bd88fe0
feature: introduce source generator for enum code generation
Meir017 Oct 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NUnit" Version="3.13.3" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.9.2" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />
Expand All @@ -23,5 +25,6 @@
<PackageVersion Include="StackExchange.Redis" Version="2.7.33" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
<PackageVersion Include="System.Interactive.Async" Version="6.0.1" />

</ItemGroup>
</Project>
13 changes: 12 additions & 1 deletion Garnet.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31808.319
MinimumVisualStudioVersion = 10.0.40219.1
Expand Down Expand Up @@ -96,6 +96,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandInfoUpdater", "playg
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "playground\SampleModule\SampleModule.csproj", "{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Garnet.gen", "libs\gen\Garnet.gen.csproj", "{E248012E-1D19-4114-924F-EFC9E859C4CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -280,6 +282,14 @@ Global
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|Any CPU.Build.0 = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.ActiveCfg = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.Build.0 = Release|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.ActiveCfg = Debug|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Debug|x64.Build.0 = Debug|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|Any CPU.Build.0 = Release|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.ActiveCfg = Release|Any CPU
{E248012E-1D19-4114-924F-EFC9E859C4CD}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -309,6 +319,7 @@ Global
{9F6E4734-6341-4A9C-A7FF-636A39D8BEAD} = {346A5A53-51E4-4A75-B7E6-491D950382CE}
{9BE474A2-1547-43AC-B4F2-FB48A01FA995} = {69A71E2C-00E3-42F3-854E-BE157A24834E}
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F} = {69A71E2C-00E3-42F3-854E-BE157A24834E}
{E248012E-1D19-4114-924F-EFC9E859C4CD} = {147FCE31-EC09-4C90-8E4D-37CA87ED18C3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2C02C405-4798-41CA-AF98-61EDFEF6772E}
Expand Down
113 changes: 0 additions & 113 deletions libs/common/EnumUtils.cs

This file was deleted.

15 changes: 15 additions & 0 deletions libs/common/GenerateEnumUtilsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;

namespace Garnet.common
{
/// <summary>
/// Specifies that utility methods for generating enum descriptions should be generated for the target enum.
/// </summary>
[AttributeUsage(AttributeTargets.Enum)]
public sealed class GenerateEnumDescriptionUtilsAttribute : Attribute
{
}
}
184 changes: 184 additions & 0 deletions libs/gen/EnumsSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System.CodeDom.Compiler;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Garnet;

[Generator]
public class EnumsSourceGenerator : IIncrementalGenerator
{
const string GeneratedClassName = "EnumUtils";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var enumDetails = context.SyntaxProvider
.ForAttributeWithMetadataName(
"Garnet.common.GenerateEnumDescriptionUtilsAttribute",
predicate: static (_, _) => true,
Meir017 marked this conversation as resolved.
Show resolved Hide resolved
transform: static (ctx, _) => TransformEnumDetails((EnumDeclarationSyntax)ctx.TargetNode, ctx.SemanticModel)
).WithComparer(new EnumDetailsComparer());
var enumUtils = enumDetails.Select(static (details, _) => Execute(details));

context.RegisterSourceOutput(enumUtils, (ctx, source) => ctx.AddSource($"{GeneratedClassName}.{source.EnumName}.g.cs", source.ClassSource));
}

private static EnumDetails TransformEnumDetails(EnumDeclarationSyntax enumDeclaration, SemanticModel semanticModel)
{
var namespaceDeclaration = enumDeclaration.FirstAncestorOrSelf<NamespaceDeclarationSyntax>();
var enumName = enumDeclaration.Identifier.Text;

var values = enumDeclaration.Members
.Select(m => (
m.Identifier.Text,
semanticModel.GetOperation(m.EqualsValue!.Value)!.ConstantValue.Value,
Description: m.AttributeLists
.SelectMany(al => al.Attributes).Where(a => a.Name.ToString() == "Description")
.SingleOrDefault()?.ArgumentList?.Arguments.Single().ToString()
))
.ToArray();
return new EnumDetails(namespaceDeclaration!.Name.ToString(), enumName, values);
}

private static (string EnumName, string ClassSource) Execute(EnumDetails details)
{
using var classWriter = new IndentedTextWriter(new StringWriter());
classWriter.WriteLine("// <auto-generated>");
classWriter.WriteLine($"// This code was generated by the {nameof(EnumsSourceGenerator)} source generator.");
classWriter.WriteLine("// </auto-generated>");

classWriter.WriteLine("using System;");
classWriter.WriteLine("using System.ComponentModel;");
classWriter.WriteLine("using System.Numerics;");
classWriter.WriteLine();
classWriter.WriteLine($"namespace {details.Namespace};");
classWriter.WriteLine();
classWriter.WriteLine("/// <summary>");
classWriter.WriteLine($"/// Utility methods for enums.");
classWriter.WriteLine("/// </summary>");
classWriter.WriteLine($"public static partial class {GeneratedClassName}");
classWriter.WriteLine("{");
classWriter.Indent++;
GenerateTryParseEnumFromDescriptionMethod(classWriter, details);
classWriter.WriteLine();
GenerateGetEnumDescriptionsMethod(classWriter, details);
classWriter.Indent--;
classWriter.WriteLine("}");

return (details.EnumName, classWriter.InnerWriter.ToString()!);
}

private static void GenerateTryParseEnumFromDescriptionMethod(IndentedTextWriter classWriter, EnumDetails details)
{
classWriter.WriteLine("/// <summary>");
classWriter.WriteLine("/// Tries to parse the enum value from the description.");
classWriter.WriteLine("/// </summary>");
classWriter.WriteLine("/// <param name=\"description\">Enum description.</param>");
classWriter.WriteLine("/// <param name=\"result\">Enum value.</param>");
classWriter.WriteLine("/// <returns>True if successful.</returns>");
classWriter.WriteLine($"public static bool TryParse{details.EnumName}FromDescription(string description, out {details.EnumName} result)");
classWriter.WriteLine("{");
classWriter.Indent++;
classWriter.WriteLine("result = default;");
classWriter.WriteLine("switch (description)");
classWriter.WriteLine("{");
classWriter.Indent++;
foreach (var (name, _, description) in details.Values)
{
bool hasDescription = false;
if (description is not null)
{
hasDescription = true;
classWriter.WriteLine($"case {description}:");
}

if (!hasDescription) continue;
classWriter.Indent++;
classWriter.WriteLine($"result = {details.EnumName}.{name};");
classWriter.WriteLine("return true;");
classWriter.Indent--;
}
classWriter.Indent--;
classWriter.WriteLine("}");
classWriter.WriteLine();
classWriter.WriteLine("return false;");
classWriter.Indent--;
classWriter.WriteLine("}");
classWriter.WriteLine();
}

private static void GenerateGetEnumDescriptionsMethod(IndentedTextWriter classWriter, EnumDetails details)
{
classWriter.WriteLine("/// <summary>");
classWriter.WriteLine("/// Gets the descriptions of the set flags. Assumes the enum is a flags enum.");
classWriter.WriteLine("/// If no description exists, returns the ToString() value of the input value.");
classWriter.WriteLine("/// </summary>");
classWriter.WriteLine("/// <param name=\"value\">Enum value.</param>");
classWriter.WriteLine("/// <returns>Array of descriptions.</returns>");
classWriter.WriteLine($"public static string[] Get{details.EnumName}Descriptions({details.EnumName} value)");
classWriter.WriteLine("{");
classWriter.Indent++;
foreach (var (name, value, description) in details.Values)
{
if (!IsPow2(value))
{
var toStringValue = description ?? $"\"{name}\"";
classWriter.WriteLine($"if (value is {details.EnumName}.{name}) return [{toStringValue}];");
}
}
classWriter.WriteLine("var setFlags = BitOperations.PopCount((ulong)value);");
classWriter.WriteLine("if (setFlags == 1)");
classWriter.WriteLine("{");
classWriter.Indent++;
classWriter.WriteLine("return value switch");
classWriter.WriteLine("{");
classWriter.Indent++;
foreach (var (name, value, description) in details.Values)
{
if (description is null) continue;
if (!IsPow2(value)) continue;
classWriter.WriteLine($"{details.EnumName}.{name} => [{description}],");
}
classWriter.WriteLine($"_ => [value.ToString()],");
classWriter.Indent--;
classWriter.WriteLine("};");
classWriter.Indent--;
classWriter.WriteLine("}");
classWriter.WriteLine();
classWriter.WriteLine("var descriptions = new string[setFlags];");
classWriter.WriteLine("var index = 0;");
foreach (var (name, value, description) in details.Values)
{
if (description is null) continue;
if (!IsPow2(value)) continue;
classWriter.WriteLine($"if ((value & {details.EnumName}.{name}) != 0) descriptions[index++] = {description};");
}
classWriter.WriteLine();
classWriter.WriteLine("return descriptions;");
classWriter.Indent--;
classWriter.WriteLine("}");
}

static bool IsPow2(object? value)
{
if (value is int x) return x != 0 && (x & (x - 1)) == 0;
return false;
}

private record struct EnumDetails(string Namespace, string EnumName, (string Name, object? Value, string? Description)[] Values);

private class EnumDetailsComparer : IEqualityComparer<EnumDetails>
{
public bool Equals(EnumDetails x, EnumDetails y) => x.EnumName.Equals(y.EnumName) && x.Namespace.Equals(y.Namespace) && x.Values.SequenceEqual(y.Values);
public int GetHashCode(EnumDetails obj)
{
var hash = new HashCode();
hash.Add(obj.EnumName);
hash.Add(obj.Namespace);
foreach (var value in obj.Values)
{
hash.Add(value);
}
return hash.ToHashCode();
}
}
}
21 changes: 21 additions & 0 deletions libs/gen/Garnet.gen.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>Latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
Loading